python-voiceio 1.14.0__tar.gz → 1.15.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.14.0/python_voiceio.egg-info → python_voiceio-1.15.1}/PKG-INFO +1 -1
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/pyproject.toml +1 -1
- {python_voiceio-1.14.0 → python_voiceio-1.15.1/python_voiceio.egg-info}/PKG-INFO +1 -1
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/python_voiceio.egg-info/SOURCES.txt +1 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_health.py +26 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_learn_pipeline.py +26 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_presence.py +1 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_report.py +19 -1
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_server.py +35 -7
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_sherpa.py +6 -1
- python_voiceio-1.15.1/tests/test_tray.py +58 -0
- python_voiceio-1.15.1/voiceio/__init__.py +1 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/cli/learn.py +2 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/core/decoders/sherpa.py +5 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/health.py +13 -1
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/presence.py +4 -1
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/report.py +11 -2
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/server/__main__.py +1 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/server/app.py +25 -12
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/server/config.py +4 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/setup/steps.py +2 -1
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/tray/__init__.py +15 -1
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/tray/_pystray.py +3 -0
- python_voiceio-1.14.0/voiceio/__init__.py +0 -1
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/LICENSE +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/README.md +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/python_voiceio.egg-info/requires.txt +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/setup.cfg +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_announce.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_app_wiring.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_audio_quality.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_audio_source.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_backend_probes.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_cancel.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_cli.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_clipboard_read.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_clipboard_typer.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_commands.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_concurrency_lockdown.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_config.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_context_terms.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_corrections.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_dead_mic.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_decoders.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_engine.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_evdev_uaccess.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_fallback.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_fcitx_bridge.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_feedback_stream.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_focus.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_hints.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_history.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_hotkey_modes.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_ibus_engine_limits.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_ibus_engine_listener.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_ibus_pending.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_ibus_pids.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_ibus_ping.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_ibus_session.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_ibus_textlimit.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_ibus_typer.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_idle_audio.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_learn_clean.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_learn_import.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_learn_language.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_learn_metrics.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_learn_quiet.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_learn_resume.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_learn_review.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_learn_targets.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_llm.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_llm_api.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_mic_gain.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_modifier_guard.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_numbers.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_paste_last.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_pipeline.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_platform.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_postcorrect.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_postprocess.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_prebuffer.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_prompt.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_quiet_audio.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_retention.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_robustness.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_security_hardening.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_service.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_setup.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_spool.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_streaming.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_suggest.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_tokens.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_transcriber.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_tts.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_typer_manager.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_vad.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_vocabulary.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_wordfreq.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/tests/test_worker_decode.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/__main__.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/announce.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/app.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/audio_source.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/backends.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/cli/__init__.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/cli/configcmd.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/cli/corrections.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/cli/doctor.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/cli/history.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/cli/models.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/cli/servicecmd.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/cli/uninstall.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/cli/vocab.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/clipboard_read.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/commands.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/config.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/consent.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/context_terms.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/core/__init__.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/core/bias.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/core/decoders/__init__.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/core/decoders/base.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/core/decoders/catalog.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/core/decoders/faster_whisper/__init__.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/core/decoders/fetch.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/core/decoders/lanes.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/core/decoders/whispercpp.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/core/engine.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/core/pipeline.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/corrections.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/data/60-voiceio-uaccess.rules +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/data/__init__.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/demo.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/display.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/feedback.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/focus.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/hints.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/history.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/hotkeys/socket_backend.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/ibus/engine.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/ibus/install.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/ibus/pending.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/ibus/session.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/ibus/textlimit.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/idle_audio.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/learn/__init__.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/learn/augment.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/learn/clean.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/learn/evaluate.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/learn/importer.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/learn/metrics.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/learn/quiet.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/learn/review.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/learn/store.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/learn/teacher.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/learn/train.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/llm.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/llm_api.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/mic_gain.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/models/__init__.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/numbers.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/pidlock.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/platform.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/postcorrect.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/postprocess.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/power.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/privacy.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/prompt.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/quiet_audio.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/recorder.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/retention.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/server/__init__.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/server/runtime.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/service.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/setup/__init__.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/setup/configfile.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/setup/extras.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/setup/hotkey.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/setup/noninteractive.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/setup/speech.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/setup/system.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/setup/ui.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/sounds/alert.wav +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/spool.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/streaming.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/suggest.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/tokens.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/tray/_indicator.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/tts/base.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/tts/chain.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/tts/player.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/tts/supertonic_engine.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/typers/base.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/typers/chain.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/typers/collecting.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/typers/fcitx_bridge.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/typers/ibus.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/typers/manager.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/typers/modifier_guard.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/typers/paste.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/typers/ydotool.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/ui.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/vad.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/vocab_stats.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/vocabulary.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/watchdog.py +0 -0
- {python_voiceio-1.14.0 → python_voiceio-1.15.1}/voiceio/wordfreq.py +0 -0
|
@@ -123,3 +123,29 @@ def test_learning_status_and_tips(monkeypatch):
|
|
|
123
123
|
assert st["Voice model"] == "fine-tuned on you (v0001-small-x)"
|
|
124
124
|
assert st["Recordings"].startswith("on (") and st["Scheduled learning"] == "off"
|
|
125
125
|
assert any("learn schedule on" in t for t in _accuracy_tips(st))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_silent_ibus_engine_is_not_a_warning_while_voiceio_is_stopped():
|
|
129
|
+
from voiceio.health import HealthReport, IBusStatus
|
|
130
|
+
p = Platform(os="linux", display_server="wayland", desktop="kde")
|
|
131
|
+
ib = IBusStatus(component_installed=True, daemon_running=True, voiceio_running=False)
|
|
132
|
+
text = format_report(HealthReport(platform=p, ibus_checks=ib))
|
|
133
|
+
line = next(ln for ln in text.splitlines() if "Engine answers" in ln)
|
|
134
|
+
assert "⚠" not in line and "isn't running" in line
|
|
135
|
+
ib.voiceio_running = True
|
|
136
|
+
line = next(ln for ln in format_report(HealthReport(platform=p, ibus_checks=ib)).splitlines()
|
|
137
|
+
if "Engine answers" in ln)
|
|
138
|
+
assert "⚠" in line and "service restart" in line
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_check_ibus_asks_about_voiceio_only_when_the_engine_is_silent():
|
|
142
|
+
from voiceio import health
|
|
143
|
+
with patch("voiceio.ibus.install.component_installed", return_value=True), \
|
|
144
|
+
patch("voiceio.ibus.install.daemon_running", return_value=True), \
|
|
145
|
+
patch("voiceio.ibus.ping_engine", return_value=False), \
|
|
146
|
+
patch("voiceio.pidlock.daemon_alive", return_value=False) as alive:
|
|
147
|
+
assert health._check_ibus().voiceio_running is False
|
|
148
|
+
with patch("voiceio.ibus.ping_engine", return_value=True):
|
|
149
|
+
alive.reset_mock()
|
|
150
|
+
assert health._check_ibus().voiceio_running is True
|
|
151
|
+
alive.assert_not_called()
|
|
@@ -307,6 +307,32 @@ def test_status_shows_scores_and_suggests_a_winning_run(capsys):
|
|
|
307
307
|
assert "next: voiceio learn promote v0001-small-x" in out
|
|
308
308
|
|
|
309
309
|
|
|
310
|
+
def test_status_never_suggests_whisper_over_another_decoder(capsys):
|
|
311
|
+
import argparse
|
|
312
|
+
|
|
313
|
+
from voiceio import config
|
|
314
|
+
from voiceio.cli import learn as cli
|
|
315
|
+
config.CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
316
|
+
config.CONFIG_PATH.write_text('[model]\nname = "parakeet-tdt-0.6b-v2"\n')
|
|
317
|
+
_clip("s.wav", 3, "x")
|
|
318
|
+
store.add_label("s.wav", [Segment(0, 3, "hi")], "teacher:fake")
|
|
319
|
+
ds = store.build_dataset()
|
|
320
|
+
run = store.learn_dir() / "models" / "v0001-small-x"
|
|
321
|
+
(run / "ct2").mkdir(parents=True)
|
|
322
|
+
runs = store.learn_dir() / "runs"
|
|
323
|
+
runs.mkdir()
|
|
324
|
+
for i, (dec, wer, rec) in enumerate([("sherpa:parakeet-tdt-0.6b-v2", 0.10, 0.5),
|
|
325
|
+
(f"faster-whisper:{run / 'ct2'}", 0.08, 0.8)]):
|
|
326
|
+
(runs / f"eval-2026010{i}.json").write_text(json.dumps({
|
|
327
|
+
"decoder": dec, "dataset": ds.name, "mode": "offline", "wer": wer,
|
|
328
|
+
"term_recall": rec, "repetitions": 0, "clips": 5, "gold": 0,
|
|
329
|
+
"metrics": metrics.METRICS_VERSION}))
|
|
330
|
+
cli.cmd_status(argparse.Namespace())
|
|
331
|
+
out = capsys.readouterr().out
|
|
332
|
+
assert "parakeet-tdt-0.6b-v2 (active)" in out and "WER 10.0%" in out
|
|
333
|
+
assert "next: voiceio learn promote" not in out
|
|
334
|
+
|
|
335
|
+
|
|
310
336
|
def test_paired_comparison_reports_direction_and_uncertainty():
|
|
311
337
|
clips = [f"{i}.wav" for i in range(20)]
|
|
312
338
|
cur = {"per_clip": [{"file": f, "wer": 0.10} for f in clips]}
|
|
@@ -38,6 +38,7 @@ AWAY = 600
|
|
|
38
38
|
(Probe(idle=1800), False, "away 30 min"),
|
|
39
39
|
(Probe(idle=1800, inhibited=True), True, "keeping the session awake"), # a video
|
|
40
40
|
(Probe(idle=1800, inhibited=True, locked=True), False, "away"),
|
|
41
|
+
(Probe(idle=3 * 3600, inhibited=True), False, "away 180 min"), # an agent, not a viewer
|
|
41
42
|
# No idle monitor: logind decides, and "can't tell" means in use.
|
|
42
43
|
(Probe(), True, "can't tell"),
|
|
43
44
|
(Probe(session=SeatSession(exists=False)), False, "nobody is logged in"),
|
|
@@ -9,7 +9,7 @@ import pytest
|
|
|
9
9
|
|
|
10
10
|
from voiceio import report
|
|
11
11
|
from voiceio.config import Config
|
|
12
|
-
from voiceio.health import BackendStatus, HealthReport
|
|
12
|
+
from voiceio.health import BackendStatus, HealthReport, IBusStatus
|
|
13
13
|
from voiceio.platform import Platform
|
|
14
14
|
|
|
15
15
|
FAKE_HOME = "/home/zelda-tester"
|
|
@@ -104,6 +104,16 @@ def test_report_lists_failures_and_warnings():
|
|
|
104
104
|
assert "insecure permissions on 2 file(s)" in text
|
|
105
105
|
|
|
106
106
|
|
|
107
|
+
def test_silent_ibus_engine_is_expected_while_voiceio_is_stopped():
|
|
108
|
+
# Two 1.15.0 reports warned "ibus engine ping" only because voiceio wasn't running.
|
|
109
|
+
ib = IBusStatus(component_installed=True, daemon_running=True, env_persisted=True)
|
|
110
|
+
stopped = report.build_report(_health(ibus_checks=IBusStatus(**{**vars(ib), "voiceio_running": False})),
|
|
111
|
+
cfg=Config())
|
|
112
|
+
assert "engine ping" not in stopped and "voiceio not running" in stopped
|
|
113
|
+
running = report.build_report(_health(ibus_checks=ib), cfg=Config())
|
|
114
|
+
assert "ibus engine ping" in running and "voiceio not running" not in running
|
|
115
|
+
|
|
116
|
+
|
|
107
117
|
def test_comment_is_one_bounded_line():
|
|
108
118
|
text = report.build_report(_health(), comment="a\nb\tc " + "x" * 1000, cfg=Config())
|
|
109
119
|
line = next(ln for ln in text.splitlines() if ln.startswith("comment:"))
|
|
@@ -111,6 +121,14 @@ def test_comment_is_one_bounded_line():
|
|
|
111
121
|
assert len(line) <= len("comment: ") + report.MAX_COMMENT
|
|
112
122
|
|
|
113
123
|
|
|
124
|
+
def test_comment_drops_terminal_escapes():
|
|
125
|
+
# A real 1.15.0 report arrived with "comment: \x1bv" (Alt+V at the prompt).
|
|
126
|
+
text = report.build_report(_health(), comment="\x1bv", cfg=Config())
|
|
127
|
+
assert "comment:" not in text and "\x1b" not in text
|
|
128
|
+
text = report.build_report(_health(), comment="works\x1b[D\x1b[C great\x07", cfg=Config())
|
|
129
|
+
assert "comment: works great" in text.splitlines()
|
|
130
|
+
|
|
131
|
+
|
|
114
132
|
# ── posting ──
|
|
115
133
|
|
|
116
134
|
class _Resp(io.BytesIO):
|
|
@@ -310,6 +310,7 @@ def test_ws_ready_partial_final_sequence():
|
|
|
310
310
|
assert final["text"] == "Hello world."
|
|
311
311
|
assert final["raw"] == "hello world raw"
|
|
312
312
|
assert isinstance(final["ms"], int)
|
|
313
|
+
assert final["cut_short"] is False and "why" not in final
|
|
313
314
|
assert engine.committed == ["Hello world."] # learned from; kept if opted in
|
|
314
315
|
|
|
315
316
|
|
|
@@ -348,8 +349,9 @@ def test_ws_client_disconnect_still_finalizes():
|
|
|
348
349
|
assert engine.sessions[0].stopped_with is not None
|
|
349
350
|
|
|
350
351
|
|
|
351
|
-
def
|
|
352
|
+
def test_ws_duration_cap_finalizes_what_fits_marked_cut_short():
|
|
352
353
|
cfg = ServerConfig(max_audio_secs=0.05) # 800 samples
|
|
354
|
+
engine = FakeEngine()
|
|
353
355
|
|
|
354
356
|
async def go(client):
|
|
355
357
|
ws = await client.ws_connect("/ws/transcribe")
|
|
@@ -361,11 +363,36 @@ def test_ws_enforces_the_duration_cap():
|
|
|
361
363
|
await ws.close()
|
|
362
364
|
return frames
|
|
363
365
|
|
|
364
|
-
frames = run_with_client(go, cfg=cfg)
|
|
365
|
-
|
|
366
|
-
assert "
|
|
367
|
-
#
|
|
368
|
-
assert
|
|
366
|
+
frames = run_with_client(go, cfg=cfg, engine=engine)
|
|
367
|
+
final = frames[-1]
|
|
368
|
+
assert final["type"] == "final" and final["text"] == "Hello world."
|
|
369
|
+
# The words are kept, and the client is told they are not the whole note.
|
|
370
|
+
assert final["cut_short"] is True and "limit" in final["why"]
|
|
371
|
+
assert len(engine.sessions[0].stopped_with) == 800 # only what fits is decoded
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def test_ws_stalled_stream_is_finalized_with_what_it_has():
|
|
375
|
+
"""A client that goes quiet without a stop (a starved or frozen sender)
|
|
376
|
+
still gets a transcript of the audio it sent."""
|
|
377
|
+
cfg = ServerConfig(stall_secs=0.2)
|
|
378
|
+
engine = FakeEngine()
|
|
379
|
+
|
|
380
|
+
async def go(client):
|
|
381
|
+
ws = await client.ws_connect("/ws/transcribe")
|
|
382
|
+
await ws.receive_json()
|
|
383
|
+
await ws.send_bytes(_pcm(800))
|
|
384
|
+
frames = []
|
|
385
|
+
async for msg in ws: # no stop: the server ends it
|
|
386
|
+
frames.append(json.loads(msg.data))
|
|
387
|
+
await ws.close()
|
|
388
|
+
return frames
|
|
389
|
+
|
|
390
|
+
frames = run_with_client(go, cfg=cfg, engine=engine)
|
|
391
|
+
final = frames[-1]
|
|
392
|
+
assert final["type"] == "final" and final["text"] == "Hello world."
|
|
393
|
+
assert final["cut_short"] is True and "no audio or stop" in final["why"]
|
|
394
|
+
assert len(engine.sessions[0].stopped_with) == 800
|
|
395
|
+
assert engine.committed == ["Hello world."]
|
|
369
396
|
|
|
370
397
|
|
|
371
398
|
def test_ws_second_concurrent_session_is_refused():
|
|
@@ -439,11 +466,12 @@ def test_config_from_env(monkeypatch):
|
|
|
439
466
|
monkeypatch.setenv("VOICE_MODEL", "base")
|
|
440
467
|
monkeypatch.setenv("VOICE_THREADS", "2")
|
|
441
468
|
monkeypatch.setenv("VOICE_MAX_SECS", "60")
|
|
469
|
+
monkeypatch.setenv("VOICE_STALL_SECS", "45")
|
|
442
470
|
monkeypatch.setenv("VOICE_TTS_ALLOW_NETWORK", "1")
|
|
443
471
|
cfg = ServerConfig.from_env()
|
|
444
472
|
assert (cfg.host, cfg.port, cfg.model) == ("0.0.0.0", 8790, "base")
|
|
445
473
|
assert cfg.threads == "2"
|
|
446
|
-
assert cfg.max_audio_secs == 60.0
|
|
474
|
+
assert (cfg.max_audio_secs, cfg.stall_secs) == (60.0, 45.0)
|
|
447
475
|
assert cfg.tts_allow_network is True
|
|
448
476
|
|
|
449
477
|
|
|
@@ -41,6 +41,10 @@ class FakeSherpa:
|
|
|
41
41
|
self.rec = {"hotwords": hotwords, "samples": 0}
|
|
42
42
|
fake.streams.append(self.rec)
|
|
43
43
|
|
|
44
|
+
def set_option(self, key, value):
|
|
45
|
+
assert isinstance(value, str)
|
|
46
|
+
self.rec[key] = value
|
|
47
|
+
|
|
44
48
|
def accept_waveform(self, sr, a):
|
|
45
49
|
assert sr == 16000 and a.dtype == np.float32
|
|
46
50
|
self.rec["samples"] = len(a)
|
|
@@ -114,6 +118,7 @@ def test_one_recognizer_and_per_stream_pretokenized_proper_nouns(fake, tmp_path)
|
|
|
114
118
|
assert out.text == "hello world"
|
|
115
119
|
# Lowercase (mined jargon) and unusable terms never become hotwords.
|
|
116
120
|
assert fake.streams[-1]["hotwords"] == "▁Kal shi/▁Hy per"
|
|
121
|
+
assert fake.streams[-1]["length_reward"] == "0.5" # per stream, as upstream reads it
|
|
117
122
|
dec.decode(_audio(2), bias=Bias(hotwords="Hyper"))
|
|
118
123
|
dec.decode(_audio(2), bias=Bias())
|
|
119
124
|
assert [s["hotwords"] for s in fake.streams[1:]] == ["▁Hy per", None]
|
|
@@ -135,7 +140,7 @@ def test_without_the_tdt_fix_decodes_greedy_and_unbiased(fake, monkeypatch, tmp_
|
|
|
135
140
|
assert dec.decode(_audio(1), bias=Bias(hotwords="Kalshi")).text == "hello world"
|
|
136
141
|
kw = fake.created[-1]
|
|
137
142
|
assert kw["decoding_method"] == "greedy_search" and "length_reward" not in kw
|
|
138
|
-
assert fake.streams[-1]["hotwords"] is None
|
|
143
|
+
assert fake.streams[-1]["hotwords"] is None and "length_reward" not in fake.streams[-1]
|
|
139
144
|
|
|
140
145
|
|
|
141
146
|
def test_long_audio_is_decoded_in_pieces_of_at_most_30s(fake, tmp_path):
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Tray backend probing: what doctor/setup promise must match what start() does."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import builtins
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from voiceio import tray
|
|
12
|
+
from voiceio.tray import _pystray
|
|
13
|
+
|
|
14
|
+
linux = pytest.mark.skipif(not sys.platform.startswith("linux"), reason="linux probe")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@linux
|
|
18
|
+
def test_appindicator_wins():
|
|
19
|
+
with patch.object(tray, "_find_system_python", return_value="/usr/bin/python3"):
|
|
20
|
+
assert tray.probe_availability() == (True, "AppIndicator3 available", "")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@linux
|
|
24
|
+
def test_pystray_xembed_fallback_counts_as_a_tray():
|
|
25
|
+
# KDE/LXQt users without python3-gi got "no tray" though start() falls back to pystray.
|
|
26
|
+
with patch.object(tray, "_find_system_python", return_value=None), \
|
|
27
|
+
patch.object(tray, "_pystray_backend", return_value="xorg"):
|
|
28
|
+
ok, reason, hint = tray.probe_availability()
|
|
29
|
+
assert ok and "no menu" in reason and "for the full menu" in hint
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@linux
|
|
33
|
+
def test_no_backend_at_all():
|
|
34
|
+
with patch.object(tray, "_find_system_python", return_value=None), \
|
|
35
|
+
patch.object(tray, "_pystray_backend", return_value=None):
|
|
36
|
+
ok, reason, hint = tray.probe_availability()
|
|
37
|
+
assert not ok and "AppIndicator3" in reason and hint.startswith("install:")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _import_raising(exc):
|
|
41
|
+
real = builtins.__import__
|
|
42
|
+
|
|
43
|
+
def fake(name, *a, **kw):
|
|
44
|
+
if name == "pystray":
|
|
45
|
+
raise exc
|
|
46
|
+
return real(name, *a, **kw)
|
|
47
|
+
return fake
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _DisplayNameError(Exception):
|
|
51
|
+
"""Stands in for Xlib.error.DisplayNameError (no DISPLAY): not an ImportError."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_pystray_without_x_display_does_not_raise(monkeypatch):
|
|
55
|
+
monkeypatch.delitem(sys.modules, "pystray", raising=False)
|
|
56
|
+
with patch("builtins.__import__", _import_raising(_DisplayNameError(":0"))):
|
|
57
|
+
assert tray._pystray_backend() is None
|
|
58
|
+
assert _pystray.start(lambda: None, Path("idle.png"), []) is False
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.15.1"
|
|
@@ -211,6 +211,8 @@ def cmd_status(args: argparse.Namespace) -> None:
|
|
|
211
211
|
print(f"note {unused}")
|
|
212
212
|
print(f"nightly {_schedule_state()}")
|
|
213
213
|
promotable = _print_scores(ds, active, runs) if ds else None
|
|
214
|
+
if unused: # a Whisper fine-tune can't replace another decoder on WER alone
|
|
215
|
+
promotable = None
|
|
214
216
|
if promotable:
|
|
215
217
|
nxt = f"promote {promotable}"
|
|
216
218
|
else:
|
|
@@ -236,6 +236,9 @@ class SherpaTransducer:
|
|
|
236
236
|
bpe_vocab=_bpe_vocab_file(self._dir, self._tokenizer))
|
|
237
237
|
if feats.length_reward:
|
|
238
238
|
kwargs.update(length_reward=length_reward)
|
|
239
|
+
# Upstream reads length_reward per stream (k2-fsa/sherpa-onnx#4084 after
|
|
240
|
+
# review); builds without that read ignore the option.
|
|
241
|
+
self._stream_reward = repr(float(length_reward)) if mbs and length_reward else ""
|
|
239
242
|
t0 = time.monotonic()
|
|
240
243
|
try:
|
|
241
244
|
self._rec = sherpa_onnx.OfflineRecognizer.from_transducer(**kwargs)
|
|
@@ -276,6 +279,8 @@ class SherpaTransducer:
|
|
|
276
279
|
for chunk in split_long(audio) if len(audio) else ():
|
|
277
280
|
stream = (self._rec.create_stream(hotwords) if hotwords
|
|
278
281
|
else self._rec.create_stream())
|
|
282
|
+
if self._stream_reward and hasattr(stream, "set_option"):
|
|
283
|
+
stream.set_option("length_reward", self._stream_reward)
|
|
279
284
|
stream.accept_waveform(SAMPLE_RATE, chunk)
|
|
280
285
|
self._rec.decode_stream(stream)
|
|
281
286
|
if text := stream.result.text.strip():
|
|
@@ -25,6 +25,8 @@ class IBusStatus:
|
|
|
25
25
|
component_installed: bool = False
|
|
26
26
|
daemon_running: bool = False
|
|
27
27
|
socket_reachable: bool = False
|
|
28
|
+
# The engine runs while voiceio does, so a silent one only matters then.
|
|
29
|
+
voiceio_running: bool = True
|
|
28
30
|
gnome_source_configured: bool = False
|
|
29
31
|
env_persisted: bool = False
|
|
30
32
|
|
|
@@ -301,6 +303,10 @@ def _check_ibus() -> IBusStatus:
|
|
|
301
303
|
# Ping, not exists(): an engine can hold a socket file yet be deaf.
|
|
302
304
|
from voiceio.ibus import ping_engine
|
|
303
305
|
status.socket_reachable = ping_engine(timeout=1.0)
|
|
306
|
+
if not status.socket_reachable:
|
|
307
|
+
from voiceio.config import PID_PATH
|
|
308
|
+
from voiceio.pidlock import daemon_alive
|
|
309
|
+
status.voiceio_running = daemon_alive(PID_PATH)
|
|
304
310
|
|
|
305
311
|
# Check GNOME input source
|
|
306
312
|
try:
|
|
@@ -433,7 +439,9 @@ def format_report(report: HealthReport) -> str:
|
|
|
433
439
|
lines.append(_check_line("Tray icon", report.tray_ok,
|
|
434
440
|
report.tray_reason if not report.tray_ok else "",
|
|
435
441
|
warn=not report.tray_ok))
|
|
436
|
-
if report.
|
|
442
|
+
if report.tray_ok and report.tray_fix_hint: # works, but degraded
|
|
443
|
+
lines[-1] += f" {DIM}{report.tray_reason}{RESET}"
|
|
444
|
+
if report.tray_fix_hint:
|
|
437
445
|
lines.append(f" {DIM}→ {report.tray_fix_hint}{RESET}")
|
|
438
446
|
|
|
439
447
|
# IBus pipeline
|
|
@@ -448,6 +456,10 @@ def format_report(report: HealthReport) -> str:
|
|
|
448
456
|
("GNOME input source", ibus.gnome_source_configured, "voiceio setup"),
|
|
449
457
|
("Env persisted (reboot-safe)", ibus.env_persisted, "voiceio setup"),
|
|
450
458
|
]:
|
|
459
|
+
if label == "Engine answers" and not ok and not ibus.voiceio_running:
|
|
460
|
+
lines.append(f" {DIM}–{RESET} {label} "
|
|
461
|
+
f"{DIM}starts with voiceio, which isn't running{RESET}")
|
|
462
|
+
continue
|
|
451
463
|
lines.append(_check_line(label, ok, hint if not ok else "",
|
|
452
464
|
warn=not ok))
|
|
453
465
|
|
|
@@ -37,6 +37,9 @@ from dataclasses import dataclass
|
|
|
37
37
|
log = logging.getLogger(__name__)
|
|
38
38
|
|
|
39
39
|
_TIMEOUT = 3
|
|
40
|
+
# No input for this long is away even when an app keeps the session awake:
|
|
41
|
+
# past a film's length, the inhibitor is a long-running job, not a viewer.
|
|
42
|
+
INHIBITED_MAX = 2 * 3600
|
|
40
43
|
|
|
41
44
|
|
|
42
45
|
@dataclass(frozen=True)
|
|
@@ -144,7 +147,7 @@ def check(away_after: float, probe: SessionProbe | None = None) -> Presence:
|
|
|
144
147
|
if probe.screen_locked():
|
|
145
148
|
return Presence(False, "screen locked")
|
|
146
149
|
return Presence(True, f"in use (last input {_minutes(idle)} ago)")
|
|
147
|
-
if probe.idle_inhibited() and not probe.screen_locked():
|
|
150
|
+
if idle < INHIBITED_MAX and probe.idle_inhibited() and not probe.screen_locked():
|
|
148
151
|
return Presence(True, "an app is keeping the session awake (a video or a call?)")
|
|
149
152
|
return Presence(False, f"away {_minutes(idle)}")
|
|
150
153
|
session = probe.seat_session()
|
|
@@ -30,6 +30,8 @@ TIMEOUT_SECS = 8.0
|
|
|
30
30
|
MAX_COMMENT = 300
|
|
31
31
|
MAX_DETAIL = 90
|
|
32
32
|
|
|
33
|
+
# A terminal escape: CSI sequences (arrow keys) or ESC plus one key (Alt+key).
|
|
34
|
+
_ESCAPE = re.compile(r"\x1b(?:\[[0-?]*[ -/]*[@-~]|.)?", re.S)
|
|
33
35
|
_IPV4 = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")
|
|
34
36
|
_IPV6 = re.compile(r"\b(?:[0-9a-f]{1,4}:){3,7}[0-9a-f]{1,4}\b", re.I)
|
|
35
37
|
_MAC = re.compile(r"\b(?:[0-9a-f]{2}[:-]){5}[0-9a-f]{2}\b", re.I)
|
|
@@ -155,7 +157,7 @@ def build_report(
|
|
|
155
157
|
if health.ibus_checks is not None:
|
|
156
158
|
ib = health.ibus_checks
|
|
157
159
|
for label, ok in (("component", ib.component_installed), ("daemon", ib.daemon_running),
|
|
158
|
-
("engine ping", ib.socket_reachable),
|
|
160
|
+
("engine ping", ib.socket_reachable or not ib.voiceio_running),
|
|
159
161
|
("env persisted", ib.env_persisted)):
|
|
160
162
|
if not ok:
|
|
161
163
|
warned.append(f"ibus {label}")
|
|
@@ -178,7 +180,9 @@ def build_report(
|
|
|
178
180
|
f"python {_py_platform.python_version()}",
|
|
179
181
|
f"backends: hotkey {hotkey.name if hotkey else 'none'} · "
|
|
180
182
|
f"typer {typer.name if typer else 'none'} · audio {audio} "
|
|
181
|
-
f"(vad {cfg.audio.vad_backend})"
|
|
183
|
+
f"(vad {cfg.audio.vad_backend})"
|
|
184
|
+
+ (" · voiceio not running" if health.ibus_checks is not None
|
|
185
|
+
and not health.ibus_checks.voiceio_running else ""),
|
|
182
186
|
f"model: {_model_label(cfg.model.name)}",
|
|
183
187
|
]
|
|
184
188
|
if unavailable:
|
|
@@ -186,6 +190,7 @@ def build_report(
|
|
|
186
190
|
lines.append("failed: " + "; ".join(failed) if failed else "failed: none")
|
|
187
191
|
if warned:
|
|
188
192
|
lines.append("warnings: " + "; ".join(warned))
|
|
193
|
+
comment = "".join(c if c.isprintable() else " " for c in _ESCAPE.sub("", comment))
|
|
189
194
|
comment = " ".join(comment.split())[:MAX_COMMENT]
|
|
190
195
|
if comment:
|
|
191
196
|
lines.append(f"comment: {comment}")
|
|
@@ -233,6 +238,10 @@ def _interactive() -> bool:
|
|
|
233
238
|
|
|
234
239
|
|
|
235
240
|
def _ask(prompt: str) -> str:
|
|
241
|
+
try:
|
|
242
|
+
import readline # noqa: F401 line editing, so arrow keys don't type escapes
|
|
243
|
+
except ImportError:
|
|
244
|
+
pass
|
|
236
245
|
try:
|
|
237
246
|
return input(prompt)
|
|
238
247
|
except (EOFError, KeyboardInterrupt):
|
|
@@ -21,6 +21,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
21
21
|
" VOICE_COMPUTE, VOICE_DEVICE compute type / device\n"
|
|
22
22
|
" VOICE_THREADS CPU threads (default 4, 0=all)\n"
|
|
23
23
|
" VOICE_MAX_BYTES, VOICE_MAX_SECS request limits\n"
|
|
24
|
+
" VOICE_STALL_SECS end a stream silent this long (30 s; 0 never)\n"
|
|
24
25
|
" VOICE_TTS_ENGINE, VOICE_TTS_VOICE, VOICE_TTS_SPEED\n"
|
|
25
26
|
" VOICE_TTS_ALLOW_NETWORK 1 to permit cloud TTS (off)\n"
|
|
26
27
|
" VOICE_LOG_LEVEL default INFO\n"
|
|
@@ -122,12 +122,15 @@ async def handle_ws_transcribe(request: web.Request) -> web.WebSocketResponse:
|
|
|
122
122
|
Server → client (all JSON text frames):
|
|
123
123
|
{"type":"ready","sample_rate":16000,"format":"pcm_s16le","channels":1}
|
|
124
124
|
{"type":"partial","text":...,"audio_secs":...}
|
|
125
|
-
{"type":"final","text":...,"raw":...,"audio_secs":...,"ms"
|
|
125
|
+
{"type":"final","text":...,"raw":...,"audio_secs":...,"ms":...,
|
|
126
|
+
"cut_short":false}
|
|
126
127
|
{"type":"error","error":...}
|
|
127
128
|
|
|
128
|
-
The final frame is sent before the socket closes. A
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
The final frame is sent before the socket closes. A stream that sends
|
|
130
|
+
neither audio nor stop for `stall_secs`, or passes `max_audio_secs`, still
|
|
131
|
+
gets a final of the audio it sent, with "cut_short":true and a "why". A
|
|
132
|
+
client that simply disconnects still gets its audio finalized server-side
|
|
133
|
+
(for vocabulary statistics), it just is not around to read the result.
|
|
131
134
|
"""
|
|
132
135
|
runtime: ServerRuntime = request.app[RUNTIME]
|
|
133
136
|
cfg: ServerConfig = request.app[CONFIG]
|
|
@@ -190,17 +193,22 @@ async def _run_stream(ws, runtime: ServerRuntime, cfg: ServerConfig, app) -> Non
|
|
|
190
193
|
"model": runtime.model_cfg.name,
|
|
191
194
|
})
|
|
192
195
|
|
|
196
|
+
cut_short = "" # why the stream ended without a stop, if it did
|
|
193
197
|
overrun = False
|
|
194
198
|
try:
|
|
195
|
-
|
|
199
|
+
while True:
|
|
200
|
+
try:
|
|
201
|
+
msg = await ws.receive(timeout=cfg.stall_secs or None)
|
|
202
|
+
except TimeoutError:
|
|
203
|
+
cut_short = f"no audio or stop for {cfg.stall_secs:g} s"
|
|
204
|
+
break
|
|
205
|
+
if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING, WSMsgType.CLOSED):
|
|
206
|
+
break
|
|
196
207
|
if msg.type == WSMsgType.BINARY:
|
|
197
208
|
total = source.feed(msg.data)
|
|
198
209
|
if total / SAMPLE_RATE > cfg.max_audio_secs:
|
|
199
210
|
overrun = True
|
|
200
|
-
|
|
201
|
-
"type": "error",
|
|
202
|
-
"error": f"audio exceeded {cfg.max_audio_secs:.0f}s limit",
|
|
203
|
-
})
|
|
211
|
+
cut_short = f"audio passed the {cfg.max_audio_secs:g} s limit"
|
|
204
212
|
break
|
|
205
213
|
elif msg.type == WSMsgType.TEXT:
|
|
206
214
|
try:
|
|
@@ -218,7 +226,9 @@ async def _run_stream(ws, runtime: ServerRuntime, cfg: ServerConfig, app) -> Non
|
|
|
218
226
|
finally:
|
|
219
227
|
source.close()
|
|
220
228
|
audio = source.get_audio_so_far()
|
|
221
|
-
|
|
229
|
+
if overrun and audio is not None: # the last frame may run far past the limit
|
|
230
|
+
audio = audio[:int(cfg.max_audio_secs * SAMPLE_RATE)]
|
|
231
|
+
secs = len(audio) / SAMPLE_RATE if audio is not None else 0.0
|
|
222
232
|
|
|
223
233
|
# stop() joins the worker thread through a final beam-search decode:
|
|
224
234
|
# seconds of CPU, and it must not block the event loop.
|
|
@@ -238,18 +248,21 @@ async def _run_stream(ws, runtime: ServerRuntime, cfg: ServerConfig, app) -> Non
|
|
|
238
248
|
outbox.put_nowait(None)
|
|
239
249
|
await pump_task
|
|
240
250
|
|
|
241
|
-
if not ws.closed
|
|
251
|
+
if not ws.closed:
|
|
242
252
|
await ws.send_json({
|
|
243
253
|
"type": "final",
|
|
244
254
|
"text": text,
|
|
245
255
|
"raw": session.raw_final_text or "",
|
|
246
256
|
"audio_secs": round(secs, 2),
|
|
247
257
|
"ms": round((time.monotonic() - t0) * 1000),
|
|
258
|
+
"cut_short": bool(cut_short),
|
|
259
|
+
**({"why": cut_short} if cut_short else {}),
|
|
248
260
|
})
|
|
249
261
|
if text: # after the reply: retention must not add to the user's wait
|
|
250
262
|
await loop.run_in_executor(app[EXECUTOR], runtime.commit, audio, text,
|
|
251
263
|
session.raw_final_text or "")
|
|
252
|
-
log.info("streaming %.1fs audio → %s", secs, describe_transcript(text)
|
|
264
|
+
log.info("streaming %.1fs audio → %s%s", secs, describe_transcript(text),
|
|
265
|
+
f" (cut short: {cut_short})" if cut_short else "")
|
|
253
266
|
if not ws.closed:
|
|
254
267
|
await ws.close()
|
|
255
268
|
|
|
@@ -59,6 +59,9 @@ class ServerConfig:
|
|
|
59
59
|
# max_audio_secs; this exists so a bad client cannot stream us to death.
|
|
60
60
|
max_bytes: int = 25 * 1024 * 1024
|
|
61
61
|
max_audio_secs: float = 120.0
|
|
62
|
+
# A stream that sends neither audio nor stop for this long is finalized
|
|
63
|
+
# with what it has (marked cut_short). 0 waits forever.
|
|
64
|
+
stall_secs: float = 30.0
|
|
62
65
|
|
|
63
66
|
# ── tts ──────────────────────────────────────────────────────────────
|
|
64
67
|
tts_engine: str = "auto"
|
|
@@ -97,6 +100,7 @@ class ServerConfig:
|
|
|
97
100
|
lanes=int(e(prefix, "LANES", "1")),
|
|
98
101
|
max_bytes=int(e(prefix, "MAX_BYTES", str(25 * 1024 * 1024))),
|
|
99
102
|
max_audio_secs=float(e(prefix, "MAX_SECS", "120")),
|
|
103
|
+
stall_secs=float(e(prefix, "STALL_SECS", "30")),
|
|
100
104
|
tts_engine=e(prefix, "TTS_ENGINE", "auto"),
|
|
101
105
|
tts_voice=e(prefix, "TTS_VOICE", ""),
|
|
102
106
|
tts_speed=float(e(prefix, "TTS_SPEED", "1.0")),
|
|
@@ -184,7 +184,8 @@ def _print_tray_check() -> bool:
|
|
|
184
184
|
from voiceio.tray import probe_availability
|
|
185
185
|
tray_ok, tray_reason, tray_fix_hint = probe_availability()
|
|
186
186
|
if tray_ok:
|
|
187
|
-
_print_check("Tray icon", True, "
|
|
187
|
+
_print_check("Tray icon", True, f"{tray_reason}; {tray_fix_hint}" if tray_fix_hint
|
|
188
|
+
else "system tray indicator available")
|
|
188
189
|
else:
|
|
189
190
|
_print_check("Tray icon", False,
|
|
190
191
|
tray_fix_hint if tray_fix_hint else tray_reason,
|
|
@@ -59,11 +59,16 @@ def probe_availability() -> tuple[bool, str, str]:
|
|
|
59
59
|
if py:
|
|
60
60
|
return True, "AppIndicator3 available", ""
|
|
61
61
|
from voiceio.platform import pkg_install
|
|
62
|
+
hint = f"install: {pkg_install('python3-gi', 'gir1.2-ayatanaappindicator3-0.1')}"
|
|
63
|
+
if _pystray_backend() == "xorg":
|
|
64
|
+
# What start() falls back to: an XEmbed icon (KDE and LXQt show
|
|
65
|
+
# it), animated, but without the menu.
|
|
66
|
+
return True, "basic icon (pystray, no menu)", f"for the full menu, {hint}"
|
|
62
67
|
return (
|
|
63
68
|
False,
|
|
64
69
|
"no python3 with GTK3/AppIndicator3 bindings "
|
|
65
70
|
"(set VOICEIO_SYSTEM_PYTHON to override)",
|
|
66
|
-
|
|
71
|
+
hint,
|
|
67
72
|
)
|
|
68
73
|
elif sys.platform == "darwin":
|
|
69
74
|
try:
|
|
@@ -82,6 +87,15 @@ def probe_availability() -> tuple[bool, str, str]:
|
|
|
82
87
|
return False, "unsupported platform", ""
|
|
83
88
|
|
|
84
89
|
|
|
90
|
+
def _pystray_backend() -> str | None:
|
|
91
|
+
"""The backend pystray picks here ("xorg", "appindicator", ...), or None."""
|
|
92
|
+
try:
|
|
93
|
+
import pystray
|
|
94
|
+
except Exception: # ImportError, or Xlib errors when there's no X display
|
|
95
|
+
return None
|
|
96
|
+
return pystray.Icon.__module__.rsplit("._", 1)[-1]
|
|
97
|
+
|
|
98
|
+
|
|
85
99
|
def _start_indicator(
|
|
86
100
|
theme_dir: Path,
|
|
87
101
|
icon_names: list[str],
|
|
@@ -35,6 +35,9 @@ def start(
|
|
|
35
35
|
except ImportError:
|
|
36
36
|
log.warning("pystray/Pillow not installed, tray icon disabled")
|
|
37
37
|
return False
|
|
38
|
+
except Exception as e: # Linux import opens an X display: none means Xlib errors
|
|
39
|
+
log.warning("pystray unusable (%s), tray icon disabled", e)
|
|
40
|
+
return False
|
|
38
41
|
|
|
39
42
|
_idle_img = Image.open(idle_path)
|
|
40
43
|
_recording_frames = [Image.open(p) for p in frame_paths]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.14.0"
|
|
File without changes
|
|
File without changes
|
{python_voiceio-1.14.0 → python_voiceio-1.15.1}/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
|