python-voiceio 1.8.2__tar.gz → 1.9.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.8.2/python_voiceio.egg-info → python_voiceio-1.9.0}/PKG-INFO +1 -1
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/pyproject.toml +1 -1
- {python_voiceio-1.8.2 → python_voiceio-1.9.0/python_voiceio.egg-info}/PKG-INFO +1 -1
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/python_voiceio.egg-info/SOURCES.txt +3 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_cancel.py +5 -2
- python_voiceio-1.9.0/tests/test_context_terms.py +253 -0
- python_voiceio-1.9.0/tests/test_learn_language.py +39 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_postprocess.py +15 -2
- python_voiceio-1.9.0/voiceio/__init__.py +1 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/app.py +31 -5
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/cli/learn.py +14 -1
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/config.py +4 -0
- python_voiceio-1.9.0/voiceio/context_terms.py +253 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/core/bias.py +52 -10
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/core/engine.py +10 -4
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/health.py +6 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/learn/teacher.py +1 -1
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/postprocess.py +32 -3
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/streaming.py +8 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/tokens.py +17 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/vocabulary.py +6 -15
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/wordfreq.py +15 -3
- python_voiceio-1.8.2/voiceio/__init__.py +0 -1
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/LICENSE +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/README.md +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/python_voiceio.egg-info/requires.txt +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/setup.cfg +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_app_wiring.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_audio_quality.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_audio_source.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_backend_probes.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_cli.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_clipboard_read.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_clipboard_typer.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_commands.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_concurrency_lockdown.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_config.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_corrections.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_decoders.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_engine.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_evdev_uaccess.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_fallback.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_fcitx_bridge.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_feedback_stream.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_focus.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_health.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_hints.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_history.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_hotkey_modes.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_ibus_engine_limits.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_ibus_engine_listener.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_ibus_pending.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_ibus_pids.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_ibus_ping.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_ibus_session.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_ibus_textlimit.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_ibus_typer.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_idle_audio.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_learn_clean.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_learn_import.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_learn_metrics.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_learn_pipeline.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_learn_quiet.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_learn_review.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_learn_targets.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_llm.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_llm_api.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_modifier_guard.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_numbers.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_paste_last.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_pipeline.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_platform.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_postcorrect.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_prebuffer.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_prompt.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_quiet_audio.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_retention.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_robustness.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_security_hardening.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_server.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_service.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_setup.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_streaming.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_suggest.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_tokens.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_transcriber.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_tts.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_typer_manager.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_vad.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_vocabulary.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_wordfreq.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_worker_decode.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/__main__.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/audio_source.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/backends.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/cli/__init__.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/cli/configcmd.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/cli/corrections.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/cli/doctor.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/cli/history.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/cli/models.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/cli/servicecmd.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/cli/uninstall.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/cli/vocab.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/clipboard_read.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/commands.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/consent.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/core/__init__.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/core/decoders/__init__.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/core/decoders/base.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/core/decoders/catalog.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/core/decoders/faster_whisper/__init__.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/core/decoders/lanes.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/core/decoders/sherpa.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/core/decoders/whispercpp.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/core/pipeline.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/corrections.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/data/__init__.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/demo.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/display.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/feedback.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/focus.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/hints.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/history.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/hotkeys/socket_backend.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/ibus/engine.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/ibus/install.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/ibus/pending.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/ibus/session.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/ibus/textlimit.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/idle_audio.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/learn/__init__.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/learn/augment.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/learn/clean.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/learn/evaluate.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/learn/importer.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/learn/metrics.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/learn/quiet.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/learn/review.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/learn/store.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/learn/train.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/llm.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/llm_api.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/models/__init__.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/numbers.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/pidlock.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/platform.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/postcorrect.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/power.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/privacy.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/prompt.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/quiet_audio.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/recorder.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/retention.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/server/__init__.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/server/__main__.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/server/app.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/server/config.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/server/runtime.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/service.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/setup/__init__.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/setup/configfile.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/setup/extras.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/setup/hotkey.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/setup/noninteractive.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/setup/speech.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/setup/steps.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/setup/system.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/setup/ui.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/suggest.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/tray/__init__.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/tray/_indicator.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/tray/_pystray.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/tts/base.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/tts/chain.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/tts/player.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/base.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/chain.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/collecting.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/fcitx_bridge.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/ibus.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/manager.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/modifier_guard.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/paste.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/ydotool.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/ui.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/vad.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/vocab_stats.py +0 -0
- {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/watchdog.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
|
|
@@ -41,6 +42,7 @@ tests/test_ibus_typer.py
|
|
|
41
42
|
tests/test_idle_audio.py
|
|
42
43
|
tests/test_learn_clean.py
|
|
43
44
|
tests/test_learn_import.py
|
|
45
|
+
tests/test_learn_language.py
|
|
44
46
|
tests/test_learn_metrics.py
|
|
45
47
|
tests/test_learn_pipeline.py
|
|
46
48
|
tests/test_learn_quiet.py
|
|
@@ -84,6 +86,7 @@ voiceio/clipboard_read.py
|
|
|
84
86
|
voiceio/commands.py
|
|
85
87
|
voiceio/config.py
|
|
86
88
|
voiceio/consent.py
|
|
89
|
+
voiceio/context_terms.py
|
|
87
90
|
voiceio/corrections.py
|
|
88
91
|
voiceio/demo.py
|
|
89
92
|
voiceio/display.py
|
|
@@ -156,14 +156,17 @@ def test_cancel_too_late_changes_nothing(tmp_path):
|
|
|
156
156
|
session = MagicMock()
|
|
157
157
|
session.cancel.return_value = False # final text already typed
|
|
158
158
|
session.stop.side_effect = lambda audio: (in_stop.set(), release.wait(5), "words")[2]
|
|
159
|
+
# The finalizer goes on to copy the text from its own thread; left real, it
|
|
160
|
+
# lands on whatever clipboard a later test has patched in.
|
|
161
|
+
vio._copy_result_async = MagicMock()
|
|
159
162
|
_finalizing(vio, tmp_path, session)
|
|
160
163
|
assert in_stop.wait(5)
|
|
161
164
|
gen = vio._generation
|
|
162
165
|
vio.cancel()
|
|
163
166
|
assert vio._state == _State.FINALIZING and vio._generation == gen
|
|
164
167
|
release.set()
|
|
165
|
-
for _ in range(
|
|
166
|
-
if vio._state == _State.IDLE:
|
|
168
|
+
for _ in range(500):
|
|
169
|
+
if vio._state == _State.IDLE and not vio._outputs:
|
|
167
170
|
break
|
|
168
171
|
time.sleep(0.01)
|
|
169
172
|
vio.engine.commit.assert_called_once()
|
|
@@ -0,0 +1,253 @@
|
|
|
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()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Fine-tuning and labeling follow the dictation language."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from voiceio.cli import learn as learn_cli
|
|
7
|
+
from voiceio.config import Config
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _with_language(monkeypatch, lang):
|
|
11
|
+
cfg = Config()
|
|
12
|
+
cfg.model.language = lang
|
|
13
|
+
monkeypatch.setattr(learn_cli, "_cfg", lambda: cfg)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_training_uses_the_dictation_language(monkeypatch):
|
|
17
|
+
"""A Spanish voice trained under the "en" token learned the wrong prompt."""
|
|
18
|
+
_with_language(monkeypatch, "es")
|
|
19
|
+
assert learn_cli._train_language() == "es"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_training_refuses_auto_with_a_hint(monkeypatch):
|
|
23
|
+
_with_language(monkeypatch, "auto")
|
|
24
|
+
with pytest.raises(SystemExit, match="model.language"):
|
|
25
|
+
learn_cli._train_language()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_teacher_detects_when_language_is_auto(monkeypatch):
|
|
29
|
+
from voiceio.learn import teacher
|
|
30
|
+
monkeypatch.setattr("voiceio.core.decoders.faster_whisper.worker.load_model", lambda *a, **k: object())
|
|
31
|
+
assert teacher.WhisperTeacher("tiny", language="auto")._language is None
|
|
32
|
+
assert teacher.WhisperTeacher("tiny", language="es")._language == "es"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_word_frequency_survives_auto_and_unknown_languages():
|
|
36
|
+
"""language = "auto" raised LookupError in vocabulary biasing."""
|
|
37
|
+
from voiceio.wordfreq import is_common, is_known
|
|
38
|
+
assert is_common("house", "auto") and is_known("casa", "es")
|
|
39
|
+
assert not is_common("house", "xx")
|
|
@@ -102,7 +102,20 @@ class TestCapitalization:
|
|
|
102
102
|
assert cleanup("hello world") == "Hello world"
|
|
103
103
|
|
|
104
104
|
def test_after_period(self):
|
|
105
|
-
assert cleanup("hello
|
|
105
|
+
assert cleanup("hello? world") == "Hello? World"
|
|
106
|
+
assert cleanup("Hello. World") == "Hello. World"
|
|
107
|
+
|
|
108
|
+
def test_lowercase_after_a_segment_stop_is_a_comma(self):
|
|
109
|
+
# Whisper ends each segment with "." and continues the sentence in
|
|
110
|
+
# lowercase; capitalizing it made a false stop permanent.
|
|
111
|
+
assert cleanup("a bit bigger. and remove the latch.") == "A bit bigger, and remove the latch."
|
|
112
|
+
assert cleanup("hello. world") == "Hello, world"
|
|
113
|
+
|
|
114
|
+
def test_abbreviations_and_dotted_names_keep_their_dot(self):
|
|
115
|
+
assert cleanup("pears etc. and more") == "Pears etc. and more"
|
|
116
|
+
assert cleanup("use e.g. this one") == "Use e.g. this one"
|
|
117
|
+
assert cleanup("go to hugo0.com. and then") == "Go to hugo0.com. and then"
|
|
118
|
+
assert cleanup("I paused... and then") == "I paused... and then"
|
|
106
119
|
|
|
107
120
|
def test_after_question_mark(self):
|
|
108
121
|
assert cleanup("really? yes") == "Really? Yes"
|
|
@@ -184,7 +197,7 @@ class TestEdgeCases:
|
|
|
184
197
|
assert result == "..."
|
|
185
198
|
|
|
186
199
|
def test_unicode_accents(self):
|
|
187
|
-
assert cleanup("caf
|
|
200
|
+
assert cleanup("café? résumé") == "Café? Résumé"
|
|
188
201
|
|
|
189
202
|
|
|
190
203
|
class TestWordMatchCompatibility:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.9.0"
|
|
@@ -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.title, screen.selection])
|
|
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
|
|
@@ -417,6 +417,16 @@ def cmd_train(args: argparse.Namespace) -> None:
|
|
|
417
417
|
f" (compare with the current model, then `voiceio learn promote {run.name}`)")
|
|
418
418
|
|
|
419
419
|
|
|
420
|
+
def _train_language() -> str:
|
|
421
|
+
"""The language a fine-tune is trained under: the one dictation decodes
|
|
422
|
+
with. "auto" has no single answer, so it has to be set first."""
|
|
423
|
+
lang = _cfg().model.language or "en"
|
|
424
|
+
if lang == "auto":
|
|
425
|
+
raise SystemExit("Fine-tuning needs one language: set it with "
|
|
426
|
+
"`voiceio config set model.language es` (your dictation language).")
|
|
427
|
+
return lang
|
|
428
|
+
|
|
429
|
+
|
|
420
430
|
def _train(ds: Path, *, base: str = "small", epochs: int = 4, synthetic_share: float = 0.3,
|
|
421
431
|
full: bool = False, remote: bool = False) -> Path | None:
|
|
422
432
|
"""Export `ds` and fine-tune on it; returns the run dir (None for --remote)."""
|
|
@@ -433,8 +443,11 @@ def _train(ds: Path, *, base: str = "small", epochs: int = 4, synthetic_share: f
|
|
|
433
443
|
return None
|
|
434
444
|
train.require_train_deps()
|
|
435
445
|
hotwords = (folder / train.HOTWORDS_FILE).read_text(encoding="utf-8")
|
|
446
|
+
# The language token the model is trained under must be the one it decodes
|
|
447
|
+
# with: training a Spanish voice under "en" taught it the wrong prompt.
|
|
436
448
|
hf = train.train(folder, run, train.TrainConfig(
|
|
437
|
-
base=base, epochs=epochs, decoder_only=False if full else None, hotwords=hotwords
|
|
449
|
+
base=base, epochs=epochs, decoder_only=False if full else None, hotwords=hotwords,
|
|
450
|
+
language=_train_language()))
|
|
438
451
|
train.convert(hf, run / "ct2")
|
|
439
452
|
return run
|
|
440
453
|
|
|
@@ -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
|