python-voiceio 1.12.0__tar.gz → 1.14.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.12.0/python_voiceio.egg-info → python_voiceio-1.14.0}/PKG-INFO +11 -2
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/README.md +10 -1
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/pyproject.toml +1 -1
- {python_voiceio-1.12.0 → python_voiceio-1.14.0/python_voiceio.egg-info}/PKG-INFO +11 -2
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/python_voiceio.egg-info/SOURCES.txt +5 -0
- python_voiceio-1.14.0/tests/test_announce.py +84 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_app_wiring.py +1 -1
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_report.py +19 -3
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_sherpa.py +6 -1
- python_voiceio-1.14.0/tests/test_spool.py +87 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_tts.py +76 -0
- python_voiceio-1.14.0/voiceio/__init__.py +1 -0
- python_voiceio-1.14.0/voiceio/announce.py +106 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/app.py +73 -9
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/cli/__init__.py +25 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/cli/models.py +4 -1
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/config.py +2 -1
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/decoders/catalog.py +18 -5
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/decoders/fetch.py +18 -8
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/feedback.py +4 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/health.py +4 -4
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/hotkeys/socket_backend.py +11 -1
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/recorder.py +11 -2
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/report.py +13 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/server/runtime.py +1 -0
- python_voiceio-1.14.0/voiceio/spool.py +116 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/tts/chain.py +4 -1
- python_voiceio-1.14.0/voiceio/tts/supertonic_engine.py +91 -0
- python_voiceio-1.12.0/voiceio/__init__.py +0 -1
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/LICENSE +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/python_voiceio.egg-info/requires.txt +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/setup.cfg +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_audio_quality.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_audio_source.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_backend_probes.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_cancel.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_cli.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_clipboard_read.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_clipboard_typer.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_commands.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_concurrency_lockdown.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_config.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_context_terms.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_corrections.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_dead_mic.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_decoders.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_engine.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_evdev_uaccess.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_fallback.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_fcitx_bridge.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_feedback_stream.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_focus.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_health.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_hints.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_history.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_hotkey_modes.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_ibus_engine_limits.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_ibus_engine_listener.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_ibus_pending.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_ibus_pids.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_ibus_ping.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_ibus_session.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_ibus_textlimit.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_ibus_typer.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_idle_audio.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_learn_clean.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_learn_import.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_learn_language.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_learn_metrics.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_learn_pipeline.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_learn_quiet.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_learn_resume.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_learn_review.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_learn_targets.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_llm.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_llm_api.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_mic_gain.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_modifier_guard.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_numbers.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_paste_last.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_pipeline.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_platform.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_postcorrect.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_postprocess.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_prebuffer.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_presence.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_prompt.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_quiet_audio.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_retention.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_robustness.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_security_hardening.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_server.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_service.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_setup.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_streaming.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_suggest.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_tokens.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_transcriber.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_typer_manager.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_vad.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_vocabulary.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_wordfreq.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_worker_decode.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/__main__.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/audio_source.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/backends.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/cli/configcmd.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/cli/corrections.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/cli/doctor.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/cli/history.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/cli/learn.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/cli/servicecmd.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/cli/uninstall.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/cli/vocab.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/clipboard_read.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/commands.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/consent.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/context_terms.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/__init__.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/bias.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/decoders/__init__.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/decoders/base.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/decoders/faster_whisper/__init__.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/decoders/lanes.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/decoders/sherpa.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/decoders/whispercpp.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/engine.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/pipeline.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/corrections.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/data/__init__.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/demo.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/display.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/focus.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/hints.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/history.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/ibus/engine.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/ibus/install.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/ibus/pending.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/ibus/session.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/ibus/textlimit.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/idle_audio.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/learn/__init__.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/learn/augment.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/learn/clean.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/learn/evaluate.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/learn/importer.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/learn/metrics.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/learn/quiet.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/learn/review.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/learn/store.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/learn/teacher.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/learn/train.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/llm.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/llm_api.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/mic_gain.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/models/__init__.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/numbers.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/pidlock.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/platform.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/postcorrect.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/postprocess.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/power.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/presence.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/privacy.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/prompt.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/quiet_audio.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/retention.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/server/__init__.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/server/__main__.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/server/app.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/server/config.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/service.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/setup/__init__.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/setup/configfile.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/setup/extras.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/setup/hotkey.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/setup/noninteractive.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/setup/speech.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/setup/steps.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/setup/system.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/setup/ui.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/sounds/alert.wav +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/streaming.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/suggest.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/tokens.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/tray/__init__.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/tray/_indicator.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/tray/_pystray.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/tts/base.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/tts/player.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/base.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/chain.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/collecting.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/fcitx_bridge.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/ibus.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/manager.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/modifier_guard.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/paste.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/ydotool.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/ui.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/vad.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/vocab_stats.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/vocabulary.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/watchdog.py +0 -0
- {python_voiceio-1.12.0 → python_voiceio-1.14.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.14.0
|
|
4
4
|
Summary: Voice dictation for Linux. Speak → text, locally, instantly.
|
|
5
5
|
Author: Hugo Montenegro
|
|
6
6
|
License-Expression: MIT
|
|
@@ -65,7 +65,7 @@ Dynamic: license-file
|
|
|
65
65
|
**[Try it in your browser at voiceio.dev](https://voiceio.dev)**: no install, the model runs in the tab.
|
|
66
66
|
|
|
67
67
|
- **Types anywhere.** Press your hotkey, speak, and the words stream into the focused app with a live underlined preview (IBus), on Wayland and X11: GNOME, KDE, Hyprland, sway, i3.
|
|
68
|
-
- **Runs on your machine.** Whisper, or NVIDIA's Parakeet (
|
|
68
|
+
- **Runs on your machine.** Whisper, or NVIDIA's Parakeet (text lands ~0.7 s after you stop talking instead of ~4.4 s, with every name you use as a hotword), decodes your speech locally. No account, no server, no telemetry; logs never contain your words.
|
|
69
69
|
- **Learns your words.** Keep your recordings (opt-in, on your disk) and voiceio fine-tunes its model on them while the machine is idle. It switches only when the new model wins on clips it never trained on. On the author's voice: 12.1% → 4.9% word errors after one night on a laptop CPU.
|
|
70
70
|
|
|
71
71
|
## Install
|
|
@@ -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.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
**[Try it in your browser at voiceio.dev](https://voiceio.dev)**: no install, the model runs in the tab.
|
|
13
13
|
|
|
14
14
|
- **Types anywhere.** Press your hotkey, speak, and the words stream into the focused app with a live underlined preview (IBus), on Wayland and X11: GNOME, KDE, Hyprland, sway, i3.
|
|
15
|
-
- **Runs on your machine.** Whisper, or NVIDIA's Parakeet (
|
|
15
|
+
- **Runs on your machine.** Whisper, or NVIDIA's Parakeet (text lands ~0.7 s after you stop talking instead of ~4.4 s, with every name you use as a hotword), decodes your speech locally. No account, no server, no telemetry; logs never contain your words.
|
|
16
16
|
- **Learns your words.** Keep your recordings (opt-in, on your disk) and voiceio fine-tunes its model on them while the machine is idle. It switches only when the new model wins on clips it never trained on. On the author's voice: 12.1% → 4.9% word errors after one night on a laptop CPU.
|
|
17
17
|
|
|
18
18
|
## Install
|
|
@@ -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.14.0
|
|
4
4
|
Summary: Voice dictation for Linux. Speak → text, locally, instantly.
|
|
5
5
|
Author: Hugo Montenegro
|
|
6
6
|
License-Expression: MIT
|
|
@@ -65,7 +65,7 @@ Dynamic: license-file
|
|
|
65
65
|
**[Try it in your browser at voiceio.dev](https://voiceio.dev)**: no install, the model runs in the tab.
|
|
66
66
|
|
|
67
67
|
- **Types anywhere.** Press your hotkey, speak, and the words stream into the focused app with a live underlined preview (IBus), on Wayland and X11: GNOME, KDE, Hyprland, sway, i3.
|
|
68
|
-
- **Runs on your machine.** Whisper, or NVIDIA's Parakeet (
|
|
68
|
+
- **Runs on your machine.** Whisper, or NVIDIA's Parakeet (text lands ~0.7 s after you stop talking instead of ~4.4 s, with every name you use as a hotword), decodes your speech locally. No account, no server, no telemetry; logs never contain your words.
|
|
69
69
|
- **Learns your words.** Keep your recordings (opt-in, on your disk) and voiceio fine-tunes its model on them while the machine is idle. It switches only when the new model wins on clips it never trained on. On the author's voice: 12.1% → 4.9% word errors after one night on a laptop CPU.
|
|
70
70
|
|
|
71
71
|
## Install
|
|
@@ -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
|
|
@@ -208,6 +212,7 @@ voiceio/tts/edge_engine.py
|
|
|
208
212
|
voiceio/tts/espeak.py
|
|
209
213
|
voiceio/tts/piper_engine.py
|
|
210
214
|
voiceio/tts/player.py
|
|
215
|
+
voiceio/tts/supertonic_engine.py
|
|
211
216
|
voiceio/typers/__init__.py
|
|
212
217
|
voiceio/typers/base.py
|
|
213
218
|
voiceio/typers/chain.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
|
|
|
@@ -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
|
|
@@ -195,7 +195,8 @@ def test_default_is_parakeet_only_when_sherpa_has_the_fix(monkeypatch):
|
|
|
195
195
|
assert catalog.default_model("en") == "small"
|
|
196
196
|
monkeypatch.setattr(sherpa, "features", lambda: SherpaFeatures(True, True, False))
|
|
197
197
|
assert catalog.default_model("en") == catalog.PARAKEET
|
|
198
|
-
assert catalog.default_model("es") ==
|
|
198
|
+
assert catalog.default_model("es") == catalog.PARAKEET_V3
|
|
199
|
+
assert catalog.default_model("ja") == "small" # outside Parakeet v3's languages
|
|
199
200
|
|
|
200
201
|
|
|
201
202
|
def test_config_resolves_unset_or_auto_name_but_an_explicit_choice_wins(monkeypatch, tmp_path):
|
|
@@ -343,6 +344,10 @@ def test_doctor_reports_the_decoder(monkeypatch):
|
|
|
343
344
|
assert "aren't used" in _learning_status(cfg)["Voice model"]
|
|
344
345
|
cfg.model.name = "small"
|
|
345
346
|
assert catalog.PARAKEET in _decoder_status(cfg)["Decoder"]
|
|
347
|
+
cfg.model.language = "es"
|
|
348
|
+
assert catalog.PARAKEET_V3 in _decoder_status(cfg)["Decoder"]
|
|
349
|
+
cfg.model.language = "ja"
|
|
350
|
+
assert _decoder_status(cfg) == {}
|
|
346
351
|
|
|
347
352
|
|
|
348
353
|
def test_eval_results_are_found_for_sherpa_models(monkeypatch, 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()
|
|
@@ -423,3 +423,79 @@ def test_piper_shutdown_releases_voice():
|
|
|
423
423
|
engine._voice = MagicMock()
|
|
424
424
|
engine.shutdown()
|
|
425
425
|
assert engine._voice is None
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# ── supertonic tests ─────────────────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
class _FakeGenerated:
|
|
431
|
+
samples = [0.0, 0.5, -1.5]
|
|
432
|
+
sample_rate = 44100
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _fake_sherpa(calls):
|
|
436
|
+
so = MagicMock()
|
|
437
|
+
|
|
438
|
+
class Tts:
|
|
439
|
+
def __init__(self, config):
|
|
440
|
+
calls["config"] = config
|
|
441
|
+
|
|
442
|
+
def generate(self, text, gen):
|
|
443
|
+
calls["text"], calls["gen"] = text, gen
|
|
444
|
+
return _FakeGenerated()
|
|
445
|
+
|
|
446
|
+
so.OfflineTts = Tts
|
|
447
|
+
so.GenerationConfig = lambda: type("G", (), {})()
|
|
448
|
+
return so
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _supertonic_dir(path):
|
|
452
|
+
from voiceio.tts import supertonic_engine as st
|
|
453
|
+
for stem in st._STEMS:
|
|
454
|
+
(path / f"{stem}.int8.onnx").write_bytes(b"")
|
|
455
|
+
for f in st._FILES:
|
|
456
|
+
(path / f).write_bytes(b"")
|
|
457
|
+
return path
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def test_supertonic_probe_needs_supertonic_in_sherpa():
|
|
461
|
+
from voiceio.tts.supertonic_engine import SupertonicEngine
|
|
462
|
+
old = MagicMock(spec=[]) # a sherpa-onnx without OfflineTtsSupertonicModelConfig
|
|
463
|
+
with patch.dict("sys.modules", {"sherpa_onnx": old}):
|
|
464
|
+
assert not SupertonicEngine().probe().ok
|
|
465
|
+
with patch.dict("sys.modules", {"sherpa_onnx": MagicMock()}):
|
|
466
|
+
assert SupertonicEngine().probe().ok
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def test_supertonic_reads_in_its_language_and_returns_int16(tmp_path):
|
|
470
|
+
from voiceio.tts import supertonic_engine as st
|
|
471
|
+
calls = {}
|
|
472
|
+
model = _supertonic_dir(tmp_path)
|
|
473
|
+
with patch.dict("sys.modules", {"sherpa_onnx": _fake_sherpa(calls)}), \
|
|
474
|
+
patch("voiceio.core.decoders.fetch.download_archive", return_value=model) as dl:
|
|
475
|
+
engine = st.SupertonicEngine(language="es")
|
|
476
|
+
audio, rate = engine.synthesize("hola", voice="", speed=0)
|
|
477
|
+
engine.synthesize("otra vez", voice="2", speed=1.2)
|
|
478
|
+
dl.assert_called_once() # loaded once, then reused
|
|
479
|
+
assert dl.call_args.args[0] == st.MODEL
|
|
480
|
+
assert calls["gen"].extra == {"lang": "es"}
|
|
481
|
+
assert calls["gen"].sid == 2 and calls["gen"].speed == 1.2
|
|
482
|
+
assert rate == 44100 and audio.dtype == np.int16
|
|
483
|
+
assert audio.tolist()[2] == -32767 # clipped, not wrapped
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def test_supertonic_auto_language_reads_english():
|
|
487
|
+
from voiceio.tts.supertonic_engine import SupertonicEngine
|
|
488
|
+
assert SupertonicEngine(language="auto")._language == "en"
|
|
489
|
+
assert SupertonicEngine()._language == "en"
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def test_supertonic_completeness_check(tmp_path):
|
|
493
|
+
from voiceio.tts import supertonic_engine as st
|
|
494
|
+
assert not st._complete(tmp_path)
|
|
495
|
+
assert st._complete(_supertonic_dir(tmp_path))
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def test_chain_passes_language_to_supertonic():
|
|
499
|
+
from voiceio.tts import chain
|
|
500
|
+
engine = chain._create("supertonic", TTSConfig(engine="supertonic", language="de"))
|
|
501
|
+
assert engine.name == "supertonic" and engine._language == "de"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.14.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
|