python-voiceio 1.13.0__tar.gz → 1.15.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.13.0/python_voiceio.egg-info → python_voiceio-1.15.0}/PKG-INFO +10 -1
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/README.md +9 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/pyproject.toml +1 -1
- {python_voiceio-1.13.0 → python_voiceio-1.15.0/python_voiceio.egg-info}/PKG-INFO +10 -1
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/python_voiceio.egg-info/SOURCES.txt +4 -0
- python_voiceio-1.15.0/tests/test_announce.py +84 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_app_wiring.py +1 -1
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_learn_pipeline.py +26 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_presence.py +1 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_report.py +19 -3
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_server.py +35 -7
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_sherpa.py +6 -1
- python_voiceio-1.15.0/tests/test_spool.py +87 -0
- python_voiceio-1.15.0/voiceio/__init__.py +1 -0
- python_voiceio-1.15.0/voiceio/announce.py +106 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/app.py +73 -9
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/cli/__init__.py +25 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/cli/learn.py +2 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/core/decoders/sherpa.py +5 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/feedback.py +4 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/hotkeys/socket_backend.py +11 -1
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/presence.py +4 -1
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/recorder.py +11 -2
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/report.py +13 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/server/__main__.py +1 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/server/app.py +25 -12
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/server/config.py +4 -0
- python_voiceio-1.15.0/voiceio/spool.py +116 -0
- python_voiceio-1.13.0/voiceio/__init__.py +0 -1
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/LICENSE +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/python_voiceio.egg-info/requires.txt +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/setup.cfg +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_audio_quality.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_audio_source.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_backend_probes.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_cancel.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_cli.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_clipboard_read.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_clipboard_typer.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_commands.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_concurrency_lockdown.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_config.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_context_terms.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_corrections.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_dead_mic.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_decoders.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_engine.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_evdev_uaccess.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_fallback.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_fcitx_bridge.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_feedback_stream.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_focus.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_health.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_hints.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_history.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_hotkey_modes.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_ibus_engine_limits.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_ibus_engine_listener.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_ibus_pending.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_ibus_pids.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_ibus_ping.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_ibus_session.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_ibus_textlimit.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_ibus_typer.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_idle_audio.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_learn_clean.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_learn_import.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_learn_language.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_learn_metrics.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_learn_quiet.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_learn_resume.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_learn_review.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_learn_targets.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_llm.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_llm_api.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_mic_gain.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_modifier_guard.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_numbers.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_paste_last.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_pipeline.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_platform.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_postcorrect.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_postprocess.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_prebuffer.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_prompt.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_quiet_audio.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_retention.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_robustness.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_security_hardening.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_service.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_setup.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_streaming.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_suggest.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_tokens.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_transcriber.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_tts.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_typer_manager.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_vad.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_vocabulary.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_wordfreq.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/tests/test_worker_decode.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/__main__.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/audio_source.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/backends.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/cli/configcmd.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/cli/corrections.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/cli/doctor.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/cli/history.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/cli/models.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/cli/servicecmd.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/cli/uninstall.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/cli/vocab.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/clipboard_read.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/commands.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/config.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/consent.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/context_terms.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/core/__init__.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/core/bias.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/core/decoders/__init__.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/core/decoders/base.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/core/decoders/catalog.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/core/decoders/faster_whisper/__init__.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/core/decoders/fetch.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/core/decoders/lanes.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/core/decoders/whispercpp.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/core/engine.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/core/pipeline.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/corrections.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/data/__init__.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/demo.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/display.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/focus.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/health.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/hints.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/history.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/ibus/engine.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/ibus/install.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/ibus/pending.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/ibus/session.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/ibus/textlimit.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/idle_audio.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/learn/__init__.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/learn/augment.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/learn/clean.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/learn/evaluate.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/learn/importer.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/learn/metrics.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/learn/quiet.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/learn/review.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/learn/store.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/learn/teacher.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/learn/train.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/llm.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/llm_api.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/mic_gain.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/models/__init__.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/numbers.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/pidlock.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/platform.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/postcorrect.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/postprocess.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/power.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/privacy.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/prompt.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/quiet_audio.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/retention.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/server/__init__.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/server/runtime.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/service.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/setup/__init__.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/setup/configfile.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/setup/extras.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/setup/hotkey.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/setup/noninteractive.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/setup/speech.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/setup/steps.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/setup/system.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/setup/ui.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/sounds/alert.wav +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/streaming.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/suggest.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/tokens.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/tray/__init__.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/tray/_indicator.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/tray/_pystray.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/tts/base.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/tts/chain.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/tts/player.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/tts/supertonic_engine.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/typers/base.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/typers/chain.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/typers/collecting.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/typers/fcitx_bridge.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/typers/ibus.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/typers/manager.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/typers/modifier_guard.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/typers/paste.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/typers/ydotool.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/ui.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/vad.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/vocab_stats.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/vocabulary.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/watchdog.py +0 -0
- {python_voiceio-1.13.0 → python_voiceio-1.15.0}/voiceio/wordfreq.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-voiceio
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.15.0
|
|
4
4
|
Summary: Voice dictation for Linux. Speak → text, locally, instantly.
|
|
5
5
|
Author: Hugo Montenegro
|
|
6
6
|
License-Expression: MIT
|
|
@@ -109,6 +109,15 @@ voiceio learn status # what it learned and how each model score
|
|
|
109
109
|
|
|
110
110
|
Scheduled learning runs daily or weekly at a time you pick, on mains power and only while you're away from the computer: it stops within seconds when you come back and resumes later where it was. You get a notification with a button to stop it. It never switches models unless the new one wins, and `voiceio learn rollback` undoes a switch. Details: [docs/learning.md](docs/learning.md).
|
|
111
111
|
|
|
112
|
+
## Let your agents talk to you
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
voiceio say --from build-bot "tests are green" # chime, notification, then it's read aloud
|
|
116
|
+
voiceio say --from deploy --urgent "I need your approval"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Running several coding agents? Have each one call `voiceio say` when it needs you, so you hear *which* one. It never talks over you: while you dictate, announcements wait (speech into an open mic would get typed). They are read in your [tts] voice, offline.
|
|
120
|
+
|
|
112
121
|
## Configure
|
|
113
122
|
|
|
114
123
|
Everything lives in `~/.config/voiceio/config.toml`; [config.example.toml](config.example.toml) documents every option. `voiceio config set section.key value` edits it in place.
|
|
@@ -56,6 +56,15 @@ voiceio learn status # what it learned and how each model score
|
|
|
56
56
|
|
|
57
57
|
Scheduled learning runs daily or weekly at a time you pick, on mains power and only while you're away from the computer: it stops within seconds when you come back and resumes later where it was. You get a notification with a button to stop it. It never switches models unless the new one wins, and `voiceio learn rollback` undoes a switch. Details: [docs/learning.md](docs/learning.md).
|
|
58
58
|
|
|
59
|
+
## Let your agents talk to you
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
voiceio say --from build-bot "tests are green" # chime, notification, then it's read aloud
|
|
63
|
+
voiceio say --from deploy --urgent "I need your approval"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Running several coding agents? Have each one call `voiceio say` when it needs you, so you hear *which* one. It never talks over you: while you dictate, announcements wait (speech into an open mic would get typed). They are read in your [tts] voice, offline.
|
|
67
|
+
|
|
59
68
|
## Configure
|
|
60
69
|
|
|
61
70
|
Everything lives in `~/.config/voiceio/config.toml`; [config.example.toml](config.example.toml) documents every option. `voiceio config set section.key value` edits it in place.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-voiceio
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.15.0
|
|
4
4
|
Summary: Voice dictation for Linux. Speak → text, locally, instantly.
|
|
5
5
|
Author: Hugo Montenegro
|
|
6
6
|
License-Expression: MIT
|
|
@@ -109,6 +109,15 @@ voiceio learn status # what it learned and how each model score
|
|
|
109
109
|
|
|
110
110
|
Scheduled learning runs daily or weekly at a time you pick, on mains power and only while you're away from the computer: it stops within seconds when you come back and resumes later where it was. You get a notification with a button to stop it. It never switches models unless the new one wins, and `voiceio learn rollback` undoes a switch. Details: [docs/learning.md](docs/learning.md).
|
|
111
111
|
|
|
112
|
+
## Let your agents talk to you
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
voiceio say --from build-bot "tests are green" # chime, notification, then it's read aloud
|
|
116
|
+
voiceio say --from deploy --urgent "I need your approval"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Running several coding agents? Have each one call `voiceio say` when it needs you, so you hear *which* one. It never talks over you: while you dictate, announcements wait (speech into an open mic would get typed). They are read in your [tts] voice, offline.
|
|
120
|
+
|
|
112
121
|
## Configure
|
|
113
122
|
|
|
114
123
|
Everything lives in `~/.config/voiceio/config.toml`; [config.example.toml](config.example.toml) documents every option. `voiceio config set section.key value` edits it in place.
|
|
@@ -7,6 +7,7 @@ python_voiceio.egg-info/dependency_links.txt
|
|
|
7
7
|
python_voiceio.egg-info/entry_points.txt
|
|
8
8
|
python_voiceio.egg-info/requires.txt
|
|
9
9
|
python_voiceio.egg-info/top_level.txt
|
|
10
|
+
tests/test_announce.py
|
|
10
11
|
tests/test_app_wiring.py
|
|
11
12
|
tests/test_audio_quality.py
|
|
12
13
|
tests/test_audio_source.py
|
|
@@ -73,6 +74,7 @@ tests/test_server.py
|
|
|
73
74
|
tests/test_service.py
|
|
74
75
|
tests/test_setup.py
|
|
75
76
|
tests/test_sherpa.py
|
|
77
|
+
tests/test_spool.py
|
|
76
78
|
tests/test_streaming.py
|
|
77
79
|
tests/test_suggest.py
|
|
78
80
|
tests/test_tokens.py
|
|
@@ -85,6 +87,7 @@ tests/test_wordfreq.py
|
|
|
85
87
|
tests/test_worker_decode.py
|
|
86
88
|
voiceio/__init__.py
|
|
87
89
|
voiceio/__main__.py
|
|
90
|
+
voiceio/announce.py
|
|
88
91
|
voiceio/app.py
|
|
89
92
|
voiceio/audio_source.py
|
|
90
93
|
voiceio/backends.py
|
|
@@ -119,6 +122,7 @@ voiceio/recorder.py
|
|
|
119
122
|
voiceio/report.py
|
|
120
123
|
voiceio/retention.py
|
|
121
124
|
voiceio/service.py
|
|
125
|
+
voiceio/spool.py
|
|
122
126
|
voiceio/streaming.py
|
|
123
127
|
voiceio/suggest.py
|
|
124
128
|
voiceio/tokens.py
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""`voiceio say`: the message format, the queue, the socket and the CLI."""
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
from voiceio import announce
|
|
7
|
+
from voiceio.announce import Message
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_encode_parse_round_trip():
|
|
11
|
+
msg = Message("tests are green", "build bot", urgent=True)
|
|
12
|
+
cmd = announce.encode(msg)
|
|
13
|
+
assert cmd.startswith("say:")
|
|
14
|
+
assert announce.parse(cmd[len("say:"):]) == msg
|
|
15
|
+
assert msg.spoken == "build bot: tests are green"
|
|
16
|
+
assert Message("hi").spoken == "hi"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_parse_rejects_junk_and_caps_length():
|
|
20
|
+
assert announce.parse("not json") is None
|
|
21
|
+
assert announce.parse('["a list"]') is None
|
|
22
|
+
assert announce.parse('{"text": " "}') is None
|
|
23
|
+
long = announce.parse('{"text": "%s", "from": "x\\n y"}' % ("a" * 5000))
|
|
24
|
+
assert len(long.text) == announce.MAX_TEXT and long.sender == "x y"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_announcer_holds_while_busy_then_speaks_in_order():
|
|
28
|
+
spoken, busy = [], threading.Event()
|
|
29
|
+
busy.set()
|
|
30
|
+
done = threading.Event()
|
|
31
|
+
|
|
32
|
+
def speak(text):
|
|
33
|
+
spoken.append(text)
|
|
34
|
+
if len(spoken) == 2:
|
|
35
|
+
done.set()
|
|
36
|
+
|
|
37
|
+
with patch.object(announce, "cue_and_notify"):
|
|
38
|
+
a = announce.Announcer(speak=speak, busy=busy.is_set)
|
|
39
|
+
a.submit(Message("one", "a"))
|
|
40
|
+
a.submit(Message("two", "b"))
|
|
41
|
+
time.sleep(0.4)
|
|
42
|
+
assert spoken == [] # held while the user dictates
|
|
43
|
+
busy.clear()
|
|
44
|
+
assert done.wait(3)
|
|
45
|
+
assert spoken == ["a: one", "b: two"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_announcer_drops_when_full():
|
|
49
|
+
hold, spoken = threading.Event(), []
|
|
50
|
+
with patch.object(announce, "cue_and_notify"):
|
|
51
|
+
a = announce.Announcer(speak=lambda t: (hold.wait(5), spoken.append(t)), busy=lambda: False)
|
|
52
|
+
results = [a.submit(Message(str(i))) for i in range(announce._QUEUE + 3)]
|
|
53
|
+
hold.set()
|
|
54
|
+
deadline = time.monotonic() + 3 # drain inside the patch: no real notifications
|
|
55
|
+
while a._queue.unfinished_tasks and len(spoken) < sum(results) and time.monotonic() < deadline:
|
|
56
|
+
time.sleep(0.05)
|
|
57
|
+
assert not all(results)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_socket_routes_payload_commands(tmp_path, monkeypatch):
|
|
61
|
+
from voiceio.hotkeys import socket_backend as sb
|
|
62
|
+
monkeypatch.setattr(sb, "SOCKET_PATH", tmp_path / "v.sock")
|
|
63
|
+
got = threading.Event()
|
|
64
|
+
payloads = []
|
|
65
|
+
hk = sb.SocketHotkey()
|
|
66
|
+
hk.handle_payload("say", lambda p: (payloads.append(p), got.set()))
|
|
67
|
+
hk.start("x", lambda: None)
|
|
68
|
+
try:
|
|
69
|
+
assert sb.send_command(announce.encode(Message("hello", "agent")))
|
|
70
|
+
assert got.wait(3)
|
|
71
|
+
finally:
|
|
72
|
+
hk.stop()
|
|
73
|
+
assert announce.parse(payloads[0]) == Message("hello", "agent")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_cli_speaks_here_without_a_daemon(tmp_path, monkeypatch, capsys):
|
|
77
|
+
import sys
|
|
78
|
+
from voiceio import cli
|
|
79
|
+
from voiceio.hotkeys import socket_backend as sb
|
|
80
|
+
monkeypatch.setattr(sb, "SOCKET_PATH", tmp_path / "absent.sock")
|
|
81
|
+
monkeypatch.setattr(sys, "argv", ["voiceio", "say", "--from", "bot", "hi", "there"])
|
|
82
|
+
with patch.object(announce, "say_here", return_value=True) as here:
|
|
83
|
+
cli.main()
|
|
84
|
+
here.assert_called_once_with(Message("hi there", "bot", False))
|
|
@@ -366,7 +366,7 @@ class TestStartCue:
|
|
|
366
366
|
|
|
367
367
|
vio.recorder.stream_health = health
|
|
368
368
|
real_start = vio.recorder.start
|
|
369
|
-
vio.recorder.start = lambda: (events.append("record"), real_start())
|
|
369
|
+
vio.recorder.start = lambda **kw: (events.append("record"), real_start(**kw))
|
|
370
370
|
vio._quiet = MagicMock()
|
|
371
371
|
return events
|
|
372
372
|
|
|
@@ -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"),
|
|
@@ -121,7 +121,23 @@ class _Resp(io.BytesIO):
|
|
|
121
121
|
return False
|
|
122
122
|
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
@pytest.fixture
|
|
125
|
+
def outside_pytest(monkeypatch):
|
|
126
|
+
"""post() refuses under pytest; these tests check what it does past that.
|
|
127
|
+
(pytest sets PYTEST_CURRENT_TEST again after fixtures, so patch the check.)"""
|
|
128
|
+
monkeypatch.setattr(report, "_test_artifact", lambda text: "")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_post_refuses_test_runs_and_mock_text(monkeypatch):
|
|
132
|
+
monkeypatch.setattr(report, "_urlopen", lambda url, timeout: pytest.fail("posted"))
|
|
133
|
+
ok, why = report.post("voiceio 1.13.0 report")
|
|
134
|
+
assert not ok and "pytest" in why
|
|
135
|
+
monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
|
|
136
|
+
ok, why = report.post("system: Ubuntu · <MagicMock name='mock.platform.desktop'>")
|
|
137
|
+
assert not ok and "mock" in why
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_post_success_reads_receipt(monkeypatch, outside_pytest):
|
|
125
141
|
seen = []
|
|
126
142
|
|
|
127
143
|
def fake(url, timeout):
|
|
@@ -138,13 +154,13 @@ def test_post_success_reads_receipt(monkeypatch):
|
|
|
138
154
|
|
|
139
155
|
@pytest.mark.parametrize("body", [b'{"ok": false, "error": "rate limited"}', b"[]", b"not json",
|
|
140
156
|
b'{"ok": true}'])
|
|
141
|
-
def test_post_bad_response_is_failure(monkeypatch, body):
|
|
157
|
+
def test_post_bad_response_is_failure(monkeypatch, outside_pytest, body):
|
|
142
158
|
monkeypatch.setattr(report, "_urlopen", lambda url, timeout: _Resp(body))
|
|
143
159
|
ok, why = report.post("x")
|
|
144
160
|
assert not ok and why
|
|
145
161
|
|
|
146
162
|
|
|
147
|
-
def test_post_network_error_never_raises():
|
|
163
|
+
def test_post_network_error_never_raises(outside_pytest):
|
|
148
164
|
# conftest refuses every connection: this is the offline path.
|
|
149
165
|
ok, why = report.post("x")
|
|
150
166
|
assert not ok and "ConnectionRefused" in why
|
|
@@ -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,87 @@
|
|
|
1
|
+
"""The crash spool: a dictation's audio survives a killed daemon."""
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from voiceio import spool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _wait(s):
|
|
10
|
+
s.flush()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_recording_is_spooled_then_removed(tmp_path):
|
|
14
|
+
s = spool.Spool(tmp_path)
|
|
15
|
+
s.begin(7, 16000, prebuffer=np.ones(160, np.float32))
|
|
16
|
+
s.write(7, np.full((320, 1), 0.5, np.float32))
|
|
17
|
+
_wait(s)
|
|
18
|
+
path = s.path(7, 16000)
|
|
19
|
+
audio = np.fromfile(path, dtype=np.float32)
|
|
20
|
+
assert len(audio) == 480 and audio[-1] == 0.5
|
|
21
|
+
assert oct(path.stat().st_mode & 0o777) == "0o600"
|
|
22
|
+
s.finish(7)
|
|
23
|
+
_wait(s)
|
|
24
|
+
assert not path.exists()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_writes_for_unknown_or_finished_recordings_are_ignored(tmp_path):
|
|
28
|
+
s = spool.Spool(tmp_path)
|
|
29
|
+
s.write(1, np.ones(10, np.float32))
|
|
30
|
+
s.finish(1)
|
|
31
|
+
_wait(s)
|
|
32
|
+
assert list(tmp_path.iterdir()) == []
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_orphans_are_files_of_dead_processes(tmp_path, monkeypatch):
|
|
36
|
+
long = np.zeros(32000, np.float32)
|
|
37
|
+
(tmp_path / "recording-999999-3-16000.f32").write_bytes(long.tobytes()) # dead
|
|
38
|
+
(tmp_path / f"recording-{os.getpid()}-4-16000.f32").write_bytes(long.tobytes()) # us
|
|
39
|
+
(tmp_path / "recording-999998-1-16000.f32").write_bytes(np.zeros(100, np.float32).tobytes())
|
|
40
|
+
(tmp_path / "notes.txt").write_text("x")
|
|
41
|
+
monkeypatch.setattr(spool, "_alive", lambda pid: pid != 999999 and pid != 999998)
|
|
42
|
+
found = spool.orphans(tmp_path)
|
|
43
|
+
assert [(p.name, len(a), r) for p, a, r in found] == [("recording-999999-3-16000.f32", 32000, 16000)]
|
|
44
|
+
assert not (tmp_path / "recording-999998-1-16000.f32").exists() # too short: dropped
|
|
45
|
+
assert (tmp_path / f"recording-{os.getpid()}-4-16000.f32").exists()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_missing_dir_has_no_orphans(tmp_path):
|
|
49
|
+
assert spool.orphans(tmp_path / "nope") == []
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_recorder_taps_every_chunk_of_a_recording():
|
|
53
|
+
from unittest.mock import MagicMock
|
|
54
|
+
from voiceio.config import AudioConfig
|
|
55
|
+
from voiceio.recorder import AudioRecorder
|
|
56
|
+
rec = AudioRecorder(AudioConfig())
|
|
57
|
+
rec._stream = MagicMock()
|
|
58
|
+
rec._ring.append(np.ones((80, 1), np.float32))
|
|
59
|
+
got = []
|
|
60
|
+
rec.start(tap=got.append)
|
|
61
|
+
rec._callback(np.full((160, 1), 0.2, np.float32), 160, None, None)
|
|
62
|
+
rec.stop()
|
|
63
|
+
rec._callback(np.full((160, 1), 0.3, np.float32), 160, None, None) # after stop: not tapped
|
|
64
|
+
assert [len(c) for c in got] == [80, 160]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_daemon_recovers_an_orphan_into_history(tmp_path, monkeypatch):
|
|
68
|
+
from types import SimpleNamespace
|
|
69
|
+
from unittest.mock import MagicMock
|
|
70
|
+
from voiceio import app, feedback, history
|
|
71
|
+
orphan = tmp_path / "recording-999999-1-16000.f32"
|
|
72
|
+
orphan.write_bytes(np.zeros(32000, np.float32).tobytes())
|
|
73
|
+
monkeypatch.setattr(spool, "spool_dir", lambda: tmp_path)
|
|
74
|
+
monkeypatch.setattr(spool, "_alive", lambda pid: False)
|
|
75
|
+
saved, told = [], []
|
|
76
|
+
monkeypatch.setattr(history, "append", lambda text, **kw: saved.append((text, kw)))
|
|
77
|
+
monkeypatch.setattr(feedback, "notify", lambda title, body, urgent=False: told.append(body))
|
|
78
|
+
vio = app.VoiceIO.__new__(app.VoiceIO)
|
|
79
|
+
vio.recorder = SimpleNamespace(sample_rate=16000)
|
|
80
|
+
vio.engine = MagicMock()
|
|
81
|
+
vio.engine.transcribe.return_value = SimpleNamespace(text="the words that were cut off", raw="r")
|
|
82
|
+
vio.cfg = SimpleNamespace(history=None)
|
|
83
|
+
vio._recover_orphans()
|
|
84
|
+
assert saved[0][0] == "the words that were cut off"
|
|
85
|
+
assert saved[0][1]["extra"] == {"recovered": True}
|
|
86
|
+
assert "paste-last" in told[0]
|
|
87
|
+
assert not orphan.exists()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.15.0"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""`voiceio say`: let an agent or a script speak to you, and say who it is.
|
|
2
|
+
|
|
3
|
+
A chime, a desktop notification titled with the sender, then the sender's
|
|
4
|
+
name and the text read aloud. The daemon queues announcements and holds them
|
|
5
|
+
while you dictate: speech played into an open mic would be typed back out,
|
|
6
|
+
and talking over you is rude anyway. Without a daemon, `voiceio say` speaks
|
|
7
|
+
in its own process instead (slower: it loads the voice first).
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import queue
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
|
|
19
|
+
log = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
MAX_TEXT = 600 # characters read aloud; an announcement, not an essay
|
|
22
|
+
_QUEUE = 8 # waiting announcements beyond this are dropped
|
|
23
|
+
_HOLD_SECS = 600 # wait at most this long for a dictation to finish
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class Message:
|
|
28
|
+
text: str
|
|
29
|
+
sender: str = ""
|
|
30
|
+
urgent: bool = False
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def spoken(self) -> str:
|
|
34
|
+
return f"{self.sender}: {self.text}" if self.sender else self.text
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def encode(msg: Message) -> str:
|
|
38
|
+
"""The control-socket command for `msg`."""
|
|
39
|
+
return "say:" + json.dumps({"text": msg.text, "from": msg.sender, "urgent": msg.urgent})
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parse(payload: str) -> Message | None:
|
|
43
|
+
try:
|
|
44
|
+
d = json.loads(payload)
|
|
45
|
+
text = " ".join(str(d.get("text", "")).split())[:MAX_TEXT]
|
|
46
|
+
sender = " ".join(str(d.get("from", "")).split())[:60]
|
|
47
|
+
except (ValueError, AttributeError):
|
|
48
|
+
return None
|
|
49
|
+
return Message(text, sender, bool(d.get("urgent"))) if text else None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def cue_and_notify(msg: Message) -> None:
|
|
53
|
+
"""The chime and the notification: who is talking, even with no voice."""
|
|
54
|
+
from voiceio import feedback
|
|
55
|
+
if feedback.open_cue_stream():
|
|
56
|
+
(feedback.play_alert_sound if msg.urgent else feedback.play_commit_sound)()
|
|
57
|
+
time.sleep(0.4) # let the chime land before the voice starts
|
|
58
|
+
feedback.notify(msg.sender or "voiceio", msg.text, urgent=msg.urgent)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Announcer:
|
|
62
|
+
"""One announcement at a time, in order, on its own thread."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, speak: Callable[[str], None], busy: Callable[[], bool],
|
|
65
|
+
hold_secs: float = _HOLD_SECS) -> None:
|
|
66
|
+
self._speak = speak
|
|
67
|
+
self._busy = busy
|
|
68
|
+
self._hold_secs = hold_secs
|
|
69
|
+
self._queue: queue.Queue[Message] = queue.Queue(maxsize=_QUEUE)
|
|
70
|
+
threading.Thread(target=self._run, name="announcer", daemon=True).start()
|
|
71
|
+
|
|
72
|
+
def submit(self, msg: Message) -> bool:
|
|
73
|
+
try:
|
|
74
|
+
self._queue.put_nowait(msg)
|
|
75
|
+
return True
|
|
76
|
+
except queue.Full:
|
|
77
|
+
log.warning("say: %d announcements waiting, dropped one from %s",
|
|
78
|
+
_QUEUE, msg.sender or "?")
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
def _run(self) -> None:
|
|
82
|
+
while True:
|
|
83
|
+
msg = self._queue.get()
|
|
84
|
+
deadline = time.monotonic() + self._hold_secs
|
|
85
|
+
while self._busy() and time.monotonic() < deadline:
|
|
86
|
+
time.sleep(0.25)
|
|
87
|
+
try:
|
|
88
|
+
cue_and_notify(msg)
|
|
89
|
+
self._speak(msg.spoken)
|
|
90
|
+
except Exception:
|
|
91
|
+
log.exception("say: announcement from %s failed", msg.sender or "?")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def say_here(msg: Message) -> bool:
|
|
95
|
+
"""No daemon: chime, notify and speak in this process. False if no TTS
|
|
96
|
+
engine works here (the chime and notification still happened)."""
|
|
97
|
+
from voiceio import config, tts
|
|
98
|
+
from voiceio.tts.player import TTSPlayer
|
|
99
|
+
cue_and_notify(msg)
|
|
100
|
+
cfg = config.load().tts
|
|
101
|
+
engine = tts.select(cfg, allow_network=False)
|
|
102
|
+
if engine is None:
|
|
103
|
+
return False
|
|
104
|
+
audio, rate = engine.synthesize(msg.spoken, cfg.voice, cfg.speed)
|
|
105
|
+
TTSPlayer().play(audio, rate)
|
|
106
|
+
return True
|