python-voiceio 1.8.3__tar.gz → 1.9.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.8.3/python_voiceio.egg-info → python_voiceio-1.9.1}/PKG-INFO +1 -1
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/pyproject.toml +1 -1
- {python_voiceio-1.8.3 → python_voiceio-1.9.1/python_voiceio.egg-info}/PKG-INFO +1 -1
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/python_voiceio.egg-info/SOURCES.txt +2 -0
- python_voiceio-1.9.1/tests/test_context_terms.py +291 -0
- python_voiceio-1.9.1/voiceio/__init__.py +1 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/app.py +31 -5
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/config.py +4 -0
- python_voiceio-1.9.1/voiceio/context_terms.py +291 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/core/bias.py +52 -10
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/core/engine.py +10 -4
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/health.py +6 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/streaming.py +8 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/tokens.py +17 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/vocabulary.py +6 -15
- python_voiceio-1.8.3/voiceio/__init__.py +0 -1
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/LICENSE +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/README.md +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/python_voiceio.egg-info/requires.txt +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/setup.cfg +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_app_wiring.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_audio_quality.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_audio_source.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_backend_probes.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_cancel.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_cli.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_clipboard_read.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_clipboard_typer.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_commands.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_concurrency_lockdown.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_config.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_corrections.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_decoders.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_engine.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_evdev_uaccess.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_fallback.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_fcitx_bridge.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_feedback_stream.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_focus.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_health.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_hints.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_history.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_hotkey_modes.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_ibus_engine_limits.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_ibus_engine_listener.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_ibus_pending.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_ibus_pids.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_ibus_ping.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_ibus_session.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_ibus_textlimit.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_ibus_typer.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_idle_audio.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_learn_clean.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_learn_import.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_learn_language.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_learn_metrics.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_learn_pipeline.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_learn_quiet.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_learn_review.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_learn_targets.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_llm.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_llm_api.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_modifier_guard.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_numbers.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_paste_last.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_pipeline.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_platform.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_postcorrect.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_postprocess.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_prebuffer.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_prompt.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_quiet_audio.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_retention.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_robustness.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_security_hardening.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_server.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_service.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_setup.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_streaming.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_suggest.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_tokens.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_transcriber.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_tts.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_typer_manager.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_vad.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_vocabulary.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_wordfreq.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/tests/test_worker_decode.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/__main__.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/audio_source.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/backends.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/cli/__init__.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/cli/configcmd.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/cli/corrections.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/cli/doctor.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/cli/history.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/cli/learn.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/cli/models.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/cli/servicecmd.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/cli/uninstall.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/cli/vocab.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/clipboard_read.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/commands.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/consent.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/core/__init__.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/core/decoders/__init__.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/core/decoders/base.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/core/decoders/catalog.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/core/decoders/faster_whisper/__init__.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/core/decoders/lanes.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/core/decoders/sherpa.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/core/decoders/whispercpp.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/core/pipeline.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/corrections.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/data/60-voiceio-uaccess.rules +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/data/__init__.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/demo.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/display.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/feedback.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/focus.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/hints.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/history.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/hotkeys/socket_backend.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/ibus/engine.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/ibus/install.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/ibus/pending.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/ibus/session.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/ibus/textlimit.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/idle_audio.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/learn/__init__.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/learn/augment.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/learn/clean.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/learn/evaluate.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/learn/importer.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/learn/metrics.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/learn/quiet.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/learn/review.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/learn/store.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/learn/teacher.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/learn/train.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/llm.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/llm_api.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/models/__init__.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/numbers.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/pidlock.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/platform.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/postcorrect.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/postprocess.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/power.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/privacy.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/prompt.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/quiet_audio.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/recorder.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/retention.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/server/__init__.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/server/__main__.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/server/app.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/server/config.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/server/runtime.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/service.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/setup/__init__.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/setup/configfile.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/setup/extras.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/setup/hotkey.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/setup/noninteractive.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/setup/speech.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/setup/steps.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/setup/system.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/setup/ui.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/suggest.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/tray/__init__.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/tray/_indicator.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/tray/_pystray.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/tts/base.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/tts/chain.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/tts/player.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/typers/base.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/typers/chain.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/typers/collecting.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/typers/fcitx_bridge.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/typers/ibus.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/typers/manager.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/typers/modifier_guard.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/typers/paste.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/typers/ydotool.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/ui.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/vad.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/vocab_stats.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/watchdog.py +0 -0
- {python_voiceio-1.8.3 → python_voiceio-1.9.1}/voiceio/wordfreq.py +0 -0
|
@@ -18,6 +18,7 @@ tests/test_clipboard_typer.py
|
|
|
18
18
|
tests/test_commands.py
|
|
19
19
|
tests/test_concurrency_lockdown.py
|
|
20
20
|
tests/test_config.py
|
|
21
|
+
tests/test_context_terms.py
|
|
21
22
|
tests/test_corrections.py
|
|
22
23
|
tests/test_decoders.py
|
|
23
24
|
tests/test_engine.py
|
|
@@ -85,6 +86,7 @@ voiceio/clipboard_read.py
|
|
|
85
86
|
voiceio/commands.py
|
|
86
87
|
voiceio/config.py
|
|
87
88
|
voiceio/consent.py
|
|
89
|
+
voiceio/context_terms.py
|
|
88
90
|
voiceio/corrections.py
|
|
89
91
|
voiceio/demo.py
|
|
90
92
|
voiceio/display.py
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""On-screen vocabulary: term extraction, budget share, capture deadline, and
|
|
2
|
+
that capture never sits on the hotkey path."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from voiceio import context_terms
|
|
12
|
+
from voiceio.context_terms import ScreenContext, capture, extract_terms
|
|
13
|
+
from voiceio.core.bias import SCREEN_TOKEN_SHARE, BiasProvider
|
|
14
|
+
from voiceio.core.decoders.base import BiasMode, Capabilities
|
|
15
|
+
from voiceio.tokens import count_tokens
|
|
16
|
+
|
|
17
|
+
HOTWORDS = Capabilities(bias=BiasMode.HOTWORDS | BiasMode.PROMPT, max_bias_tokens=120)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ── extraction ────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestExtract:
|
|
24
|
+
def test_rare_names_camelcase_acronyms(self):
|
|
25
|
+
terms = extract_terms(["Kalshi pricing - Discord",
|
|
26
|
+
"The HyperNEAT paper uses OpenRouter and the OKR doc."])
|
|
27
|
+
assert {"Kalshi", "HyperNEAT", "OpenRouter", "OKR"} <= set(terms)
|
|
28
|
+
|
|
29
|
+
def test_common_words_and_app_names_are_not_terms(self):
|
|
30
|
+
terms = extract_terms(["Inbox - Google Chrome",
|
|
31
|
+
"Please check the API before the meeting with Hugo."])
|
|
32
|
+
assert terms == []
|
|
33
|
+
|
|
34
|
+
def test_multiword_name_but_not_a_greeting(self):
|
|
35
|
+
terms = extract_terms(["", "Hey Kosh, ask Tarrant Fnordwick about it."])
|
|
36
|
+
assert "Tarrant Fnordwick" in terms
|
|
37
|
+
assert "Kosh" in terms
|
|
38
|
+
assert not any(t.startswith("Hey") for t in terms)
|
|
39
|
+
# A name and its own parts would spend tokens twice on one word.
|
|
40
|
+
assert "Tarrant" not in terms and "Fnordwick" not in terms
|
|
41
|
+
|
|
42
|
+
def test_identifiers_split_numbers_dropped(self):
|
|
43
|
+
terms = extract_terms(["", "see github.com/Hugo0/voiceio and v2 of x86_64 build 2026"])
|
|
44
|
+
assert "voiceio" in terms
|
|
45
|
+
assert not any(any(c.isdigit() for c in t) for t in terms)
|
|
46
|
+
|
|
47
|
+
def test_exclude_is_case_insensitive_and_dedupes_spellings(self):
|
|
48
|
+
terms = extract_terms(["", "Kalshi kalshi KALSHI Zorblax"], exclude=["zorblax"])
|
|
49
|
+
assert [t.lower() for t in terms] == ["kalshi"]
|
|
50
|
+
|
|
51
|
+
def test_vocabulary_long_tail_ranks_first(self):
|
|
52
|
+
terms = extract_terms(["", "Zorblax met Quuxley at Wibblecorp."],
|
|
53
|
+
prefer=["Wibblecorp"])
|
|
54
|
+
assert terms[0] == "Wibblecorp"
|
|
55
|
+
|
|
56
|
+
def test_title_beats_selection_on_ties(self):
|
|
57
|
+
terms = extract_terms(["Quuxley", "Zorblax"])
|
|
58
|
+
assert terms == ["Quuxley", "Zorblax"]
|
|
59
|
+
|
|
60
|
+
def test_repeats_outrank_one_offs(self):
|
|
61
|
+
terms = extract_terms(["", "Zorblax once. Quuxley, Quuxley, Quuxley."])
|
|
62
|
+
assert terms[0] == "Quuxley"
|
|
63
|
+
|
|
64
|
+
def test_capped(self):
|
|
65
|
+
text = " ".join(f"Zorb{chr(97 + i)}lax" for i in range(20))
|
|
66
|
+
assert len(extract_terms(["", text], max_terms=5)) == 5
|
|
67
|
+
|
|
68
|
+
def test_huge_selection_is_cheap(self):
|
|
69
|
+
text = "the quick brown fox Zorblax jumps " * 20000
|
|
70
|
+
t0 = time.monotonic()
|
|
71
|
+
assert extract_terms(["", text]) == ["Zorblax"]
|
|
72
|
+
assert time.monotonic() - t0 < 1.0
|
|
73
|
+
|
|
74
|
+
def test_empty(self):
|
|
75
|
+
assert extract_terms(["", ""]) == []
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ── budget share in the hotwords ──────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _provider(terms: list[str]) -> BiasProvider:
|
|
82
|
+
from voiceio import config
|
|
83
|
+
from voiceio.vocabulary import VocabularyLoader
|
|
84
|
+
config.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
(config.CONFIG_DIR / "vocabulary.txt").write_text("\n".join(terms) + "\n")
|
|
86
|
+
return BiasProvider(VocabularyLoader(config.ModelConfig()))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _vocab(n: int) -> list[str]:
|
|
90
|
+
# Distinct pronounceable made-up names, in file (= rank) order.
|
|
91
|
+
syll = ["ka", "zor", "bli", "mun", "tep", "vas", "quo", "rin", "lud", "fey"]
|
|
92
|
+
return [(syll[i % 10] + syll[(i // 10) % 10] + syll[(i * 7) % 10]).capitalize() + "x"
|
|
93
|
+
for i in range(n)]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class TestBudget:
|
|
97
|
+
def test_no_screen_terms_is_unchanged(self):
|
|
98
|
+
bp = _provider(_vocab(60))
|
|
99
|
+
assert bp.bias_for(HOTWORDS).hotwords == bp.bias_for(HOTWORDS, []).hotwords
|
|
100
|
+
|
|
101
|
+
def test_screen_terms_displace_only_the_vocabulary_tail(self):
|
|
102
|
+
vocab = _vocab(60)
|
|
103
|
+
bp = _provider(vocab)
|
|
104
|
+
base = bp.bias_for(HOTWORDS).hotwords.split(", ")
|
|
105
|
+
merged = bp.bias_for(HOTWORDS, ["Quuxley", "Wibblecorp"]).hotwords.split(", ")
|
|
106
|
+
assert "Quuxley" in merged and "Wibblecorp" in merged
|
|
107
|
+
kept = [t for t in merged if t in vocab]
|
|
108
|
+
assert kept == base[:len(kept)], "the best-ranked terms survive, in order"
|
|
109
|
+
assert len(kept) < len(base)
|
|
110
|
+
assert count_tokens(", ".join(merged), "small") <= 120 + 2 # join slack
|
|
111
|
+
|
|
112
|
+
def test_screen_share_is_capped(self):
|
|
113
|
+
bp = _provider(_vocab(60))
|
|
114
|
+
many = [f"Zorb{chr(97 + i)}laxian" for i in range(20)]
|
|
115
|
+
merged = bp.bias_for(HOTWORDS, many).hotwords.split(", ")
|
|
116
|
+
screen = [t for t in merged if t in many]
|
|
117
|
+
assert 0 < len(screen) < len(many)
|
|
118
|
+
assert count_tokens(", ".join(screen), "small") <= SCREEN_TOKEN_SHARE
|
|
119
|
+
# The vocabulary keeps the rest of the budget.
|
|
120
|
+
assert len(merged) - len(screen) >= 10
|
|
121
|
+
|
|
122
|
+
def test_screen_terms_skip_what_hotwords_carry(self):
|
|
123
|
+
bp = _provider(["Kalshi", "HyperNEAT"])
|
|
124
|
+
assert bp.screen_terms(["Kalshi - Discord", "Quuxley and HyperNEAT"], HOTWORDS) == ["Quuxley"]
|
|
125
|
+
|
|
126
|
+
def test_unbounded_appends(self):
|
|
127
|
+
bp = _provider(_vocab(60))
|
|
128
|
+
caps = Capabilities(bias=BiasMode.UNBOUNDED)
|
|
129
|
+
merged = bp.bias_for(caps, ["Quuxley"]).hotwords.split(", ")
|
|
130
|
+
assert merged[-1] == "Quuxley" and len(merged) == 61
|
|
131
|
+
|
|
132
|
+
def test_decoder_without_hotwords_gets_nothing(self):
|
|
133
|
+
bp = _provider(["Kalshi"])
|
|
134
|
+
assert bp.screen_terms(["Quuxley"], Capabilities(bias=BiasMode.PROMPT)) == []
|
|
135
|
+
|
|
136
|
+
def test_extraction_failure_degrades_to_nothing(self):
|
|
137
|
+
bp = _provider(["Kalshi"])
|
|
138
|
+
with patch("voiceio.context_terms.extract_terms", side_effect=RuntimeError):
|
|
139
|
+
assert bp.screen_terms(["Quuxley"], HOTWORDS) == []
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ── capture: deadline and degrade ─────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class TestCapture:
|
|
146
|
+
def test_slow_source_is_dropped_at_the_deadline(self):
|
|
147
|
+
gate = threading.Event()
|
|
148
|
+
|
|
149
|
+
def slow(_t):
|
|
150
|
+
gate.wait(5)
|
|
151
|
+
return "Zorblax"
|
|
152
|
+
|
|
153
|
+
t0 = time.monotonic()
|
|
154
|
+
ctx = capture(0.1, readers={"title": lambda _t: "Kalshi", "selection": slow})
|
|
155
|
+
elapsed = time.monotonic() - t0
|
|
156
|
+
gate.set()
|
|
157
|
+
assert elapsed < 0.5
|
|
158
|
+
assert ctx.title == "Kalshi" and ctx.selection == ""
|
|
159
|
+
|
|
160
|
+
def test_failing_source_degrades_silently(self):
|
|
161
|
+
def boom(_t):
|
|
162
|
+
raise OSError("no display")
|
|
163
|
+
ctx = capture(0.1, readers={"title": boom, "selection": lambda _t: "x"})
|
|
164
|
+
assert ctx == ScreenContext(title="", selection="x", secs=ctx.secs)
|
|
165
|
+
|
|
166
|
+
def test_no_tools_no_selection(self, monkeypatch):
|
|
167
|
+
monkeypatch.setattr(context_terms.shutil, "which", lambda _n: None)
|
|
168
|
+
assert context_terms._read_primary(0.1) == ""
|
|
169
|
+
|
|
170
|
+
def test_gnome_wayland_never_uses_wl_paste(self, monkeypatch):
|
|
171
|
+
"""wl-paste can take focus on GNOME and drop the IBus preedit."""
|
|
172
|
+
from types import SimpleNamespace
|
|
173
|
+
monkeypatch.delenv("DISPLAY", raising=False)
|
|
174
|
+
monkeypatch.setattr("voiceio.platform.detect",
|
|
175
|
+
lambda: SimpleNamespace(is_wayland=True, desktop="gnome"))
|
|
176
|
+
monkeypatch.setattr(context_terms.shutil, "which", lambda n: f"/usr/bin/{n}")
|
|
177
|
+
with patch.object(context_terms.subprocess, "run") as run:
|
|
178
|
+
assert context_terms._read_primary(0.1) == ""
|
|
179
|
+
run.assert_not_called()
|
|
180
|
+
|
|
181
|
+
def test_selection_read_has_the_deadline_as_timeout(self, monkeypatch):
|
|
182
|
+
monkeypatch.setenv("DISPLAY", ":0")
|
|
183
|
+
monkeypatch.setattr(context_terms.shutil, "which", lambda n: f"/usr/bin/{n}")
|
|
184
|
+
with patch.object(context_terms.subprocess, "run") as run:
|
|
185
|
+
run.return_value.returncode = 0
|
|
186
|
+
run.return_value.stdout = "Zorblax".encode()
|
|
187
|
+
assert context_terms._read_primary(0.15) == "Zorblax"
|
|
188
|
+
assert run.call_args.kwargs["timeout"] == 0.15
|
|
189
|
+
assert run.call_args.args[0][:2] == ["xclip", "-o"]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ── the daemon: never on the hotkey path ──────────────────────────────────
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@pytest.fixture
|
|
196
|
+
def vio():
|
|
197
|
+
from test_app_wiring import _make_vio
|
|
198
|
+
from fakes import FakeDecoder
|
|
199
|
+
dec = FakeDecoder()
|
|
200
|
+
dec.transcribe.return_value = "hello"
|
|
201
|
+
v, _, _ = _make_vio(dec)
|
|
202
|
+
v.cfg.model.screen_vocabulary = True
|
|
203
|
+
yield v, dec
|
|
204
|
+
if v._session is not None:
|
|
205
|
+
v._session.cancel()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class TestDaemon:
|
|
209
|
+
def test_capture_does_not_block_recording_start(self, vio):
|
|
210
|
+
v, dec = vio
|
|
211
|
+
gate = threading.Event()
|
|
212
|
+
captured = threading.Event()
|
|
213
|
+
|
|
214
|
+
def blocked_capture(*_a, **_k):
|
|
215
|
+
gate.wait(5)
|
|
216
|
+
captured.set()
|
|
217
|
+
return ScreenContext(title="Quuxley - Discord")
|
|
218
|
+
|
|
219
|
+
with patch("voiceio.context_terms.capture", side_effect=blocked_capture):
|
|
220
|
+
t0 = time.monotonic()
|
|
221
|
+
v.on_hotkey()
|
|
222
|
+
assert time.monotonic() - t0 < 1.0
|
|
223
|
+
assert v.recorder.is_recording, "recording started while capture blocks"
|
|
224
|
+
assert "Quuxley" not in (v._recording_bias.hotwords or "")
|
|
225
|
+
gate.set()
|
|
226
|
+
assert captured.wait(5)
|
|
227
|
+
for _ in range(100):
|
|
228
|
+
if "Quuxley" in (v._recording_bias.hotwords or ""):
|
|
229
|
+
break
|
|
230
|
+
time.sleep(0.02)
|
|
231
|
+
assert "Quuxley" in (v._recording_bias.hotwords or "")
|
|
232
|
+
assert "Quuxley" in (v._session._bias.hotwords or ""), "applied to later passes"
|
|
233
|
+
|
|
234
|
+
def test_late_capture_never_touches_the_next_recording(self, vio):
|
|
235
|
+
v, _ = vio
|
|
236
|
+
gate = threading.Event()
|
|
237
|
+
with patch("voiceio.context_terms.capture",
|
|
238
|
+
side_effect=lambda *a, **k: (gate.wait(5), ScreenContext(title="Quuxley"))[1]):
|
|
239
|
+
v.on_hotkey()
|
|
240
|
+
rid = v._recording
|
|
241
|
+
v._recording += 1 # as if this recording ended and another began
|
|
242
|
+
gate.set()
|
|
243
|
+
time.sleep(0.3)
|
|
244
|
+
v._recording = rid
|
|
245
|
+
assert "Quuxley" not in (v._recording_bias.hotwords or "")
|
|
246
|
+
|
|
247
|
+
def test_off_reads_nothing(self, vio):
|
|
248
|
+
v, _ = vio
|
|
249
|
+
v.cfg.model.screen_vocabulary = False
|
|
250
|
+
with patch("voiceio.context_terms.capture") as cap:
|
|
251
|
+
v.on_hotkey()
|
|
252
|
+
time.sleep(0.1)
|
|
253
|
+
cap.assert_not_called()
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _fake_tmux(outputs):
|
|
257
|
+
calls = []
|
|
258
|
+
|
|
259
|
+
def run(args, timeout):
|
|
260
|
+
calls.append(args)
|
|
261
|
+
return outputs.get(args[0], "")
|
|
262
|
+
return run, calls
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def test_tmux_session_names_become_words(monkeypatch):
|
|
266
|
+
"""Session names are the terminal's tabs: their slug parts are terms."""
|
|
267
|
+
from voiceio import context_terms as c
|
|
268
|
+
run, calls = _fake_tmux({"list-sessions": "s claude-swarmmemo-live\ns claude-wordle-native\nc %1\nc %2\n"})
|
|
269
|
+
monkeypatch.setattr(c, "_tmux", run)
|
|
270
|
+
monkeypatch.setattr(c.shutil, "which", lambda name: "/usr/bin/tmux")
|
|
271
|
+
text = c._read_tmux(0.2)
|
|
272
|
+
assert "swarmmemo" in text.split() and "claude-swarmmemo-live" not in text
|
|
273
|
+
# Two clients: tmux can't say which one has focus, so no pane is read.
|
|
274
|
+
assert not any(a[0] == "capture-pane" for a in calls)
|
|
275
|
+
assert "swarmmemo" in c.extract_terms([text])
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_tmux_pane_read_only_with_a_single_client(monkeypatch):
|
|
279
|
+
from voiceio import context_terms as c
|
|
280
|
+
run, calls = _fake_tmux({"list-sessions": "s work\nc %3\n", "capture-pane": "run xdotool now\n"})
|
|
281
|
+
monkeypatch.setattr(c, "_tmux", run)
|
|
282
|
+
monkeypatch.setattr(c.shutil, "which", lambda name: "/usr/bin/tmux")
|
|
283
|
+
text = c._read_tmux(0.2)
|
|
284
|
+
assert ["capture-pane", "-p", "-J", "-t", "%3"] in calls
|
|
285
|
+
assert "xdotool" in text
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_no_tmux_no_terminal_text(monkeypatch):
|
|
289
|
+
from voiceio import context_terms as c
|
|
290
|
+
monkeypatch.setattr(c.shutil, "which", lambda name: None)
|
|
291
|
+
assert c._read_tmux(0.2) == ""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.9.1"
|
|
@@ -168,6 +168,10 @@ class VoiceIO:
|
|
|
168
168
|
threading.Thread(
|
|
169
169
|
target=retention.prune, args=(cfg.data,), daemon=True,
|
|
170
170
|
).start()
|
|
171
|
+
if cfg.model.screen_vocabulary:
|
|
172
|
+
# Word frequency tables load in ~1s; not on the first dictation.
|
|
173
|
+
from voiceio.context_terms import warm
|
|
174
|
+
threading.Thread(target=warm, args=(cfg.model.language,), daemon=True).start()
|
|
171
175
|
|
|
172
176
|
# Hotkey deduplication
|
|
173
177
|
self._hotkey_lock = threading.Lock()
|
|
@@ -557,10 +561,12 @@ class VoiceIO:
|
|
|
557
561
|
# `voiceio vocab`/`corrections` apply from the next recording.
|
|
558
562
|
bias = self.engine.bias()
|
|
559
563
|
self._recording_bias = bias
|
|
560
|
-
if self.cfg.data.capture_context:
|
|
561
|
-
# Snapshot the dictation target now — focus may change by finalize
|
|
564
|
+
if self.cfg.data.capture_context or self.cfg.model.screen_vocabulary:
|
|
565
|
+
# Snapshot the dictation target now — focus may change by finalize.
|
|
566
|
+
# Off this thread: the chime and the mic never wait on it.
|
|
562
567
|
self._context_title = None
|
|
563
|
-
threading.Thread(target=self._capture_context,
|
|
568
|
+
threading.Thread(target=self._capture_context, args=(self._recording,),
|
|
569
|
+
daemon=True, name="screen-context").start()
|
|
564
570
|
self._record_start = time.monotonic()
|
|
565
571
|
self.recorder.start()
|
|
566
572
|
self.recorder.set_on_auto_stop(self._on_auto_stop)
|
|
@@ -627,9 +633,29 @@ class VoiceIO:
|
|
|
627
633
|
# No finalizer will run to release the input source, so do it here.
|
|
628
634
|
self._typers.end(rid)
|
|
629
635
|
|
|
630
|
-
def _capture_context(self) -> None:
|
|
636
|
+
def _capture_context(self, rid: int) -> None:
|
|
631
637
|
from voiceio import retention
|
|
632
|
-
|
|
638
|
+
if not self.cfg.model.screen_vocabulary:
|
|
639
|
+
self._context_title = retention.active_window_title()
|
|
640
|
+
return
|
|
641
|
+
from voiceio import context_terms
|
|
642
|
+
t0 = time.monotonic()
|
|
643
|
+
screen = context_terms.capture()
|
|
644
|
+
if self.cfg.data.capture_context:
|
|
645
|
+
self._context_title = screen.title or None
|
|
646
|
+
terms = self.engine.screen_terms(screen.texts())
|
|
647
|
+
if not terms:
|
|
648
|
+
return
|
|
649
|
+
bias = self.engine.bias(screen_terms=terms)
|
|
650
|
+
with self._hotkey_lock:
|
|
651
|
+
if self._recording != rid or self._state != _State.RECORDING:
|
|
652
|
+
return
|
|
653
|
+
self._recording_bias = bias
|
|
654
|
+
if self._session is not None:
|
|
655
|
+
self._session.set_bias(bias)
|
|
656
|
+
# A count only: these are words from the user's screen.
|
|
657
|
+
log.debug("Screen vocabulary: %d term(s) from %s, ready %.0f ms after start",
|
|
658
|
+
len(terms), "+".join(screen.sources()), (time.monotonic() - t0) * 1000)
|
|
633
659
|
|
|
634
660
|
def _retention_extra(self, audio: np.ndarray | None) -> dict:
|
|
635
661
|
"""Save the utterance audio (before transcription, so it survives
|
|
@@ -90,6 +90,10 @@ class ModelConfig:
|
|
|
90
90
|
# "modified_beam_search" (required for hotwords).
|
|
91
91
|
decoding: str = ""
|
|
92
92
|
vocabulary_file: str = "" # path to vocabulary.txt
|
|
93
|
+
# Bias each dictation toward rare names on screen: the focused window's
|
|
94
|
+
# title and the selected text (voiceio/context_terms.py). Read locally
|
|
95
|
+
# when recording starts; never stored or sent anywhere.
|
|
96
|
+
screen_vocabulary: bool = True
|
|
93
97
|
# Beam width of the passes whose text you keep (freezes and the final
|
|
94
98
|
# tail); interim previews are always greedy. 1 is greedy: faster stop.
|
|
95
99
|
beam_size: int = 5
|