python-voiceio 1.9.2__tar.gz → 1.9.3__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.9.2/python_voiceio.egg-info → python_voiceio-1.9.3}/PKG-INFO +2 -2
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/README.md +1 -1
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/pyproject.toml +1 -1
- {python_voiceio-1.9.2 → python_voiceio-1.9.3/python_voiceio.egg-info}/PKG-INFO +2 -2
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/python_voiceio.egg-info/SOURCES.txt +4 -0
- python_voiceio-1.9.3/tests/test_dead_mic.py +94 -0
- python_voiceio-1.9.3/tests/test_learn_quiet.py +247 -0
- python_voiceio-1.9.3/tests/test_learn_resume.py +243 -0
- python_voiceio-1.9.3/tests/test_presence.py +99 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_service.py +33 -1
- python_voiceio-1.9.3/voiceio/__init__.py +1 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/app.py +33 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/cli/learn.py +127 -35
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/config.py +9 -3
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/learn/evaluate.py +4 -1
- python_voiceio-1.9.3/voiceio/learn/quiet.py +172 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/learn/store.py +9 -3
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/learn/teacher.py +3 -3
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/learn/train.py +219 -44
- python_voiceio-1.9.3/voiceio/presence.py +160 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/recorder.py +38 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/service.py +28 -6
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/setup/steps.py +1 -1
- python_voiceio-1.9.2/tests/test_learn_quiet.py +0 -103
- python_voiceio-1.9.2/voiceio/__init__.py +0 -1
- python_voiceio-1.9.2/voiceio/learn/quiet.py +0 -41
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/LICENSE +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/python_voiceio.egg-info/requires.txt +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/setup.cfg +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_app_wiring.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_audio_quality.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_audio_source.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_backend_probes.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_cancel.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_cli.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_clipboard_read.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_clipboard_typer.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_commands.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_concurrency_lockdown.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_config.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_context_terms.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_corrections.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_decoders.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_engine.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_evdev_uaccess.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_fallback.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_fcitx_bridge.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_feedback_stream.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_focus.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_health.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_hints.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_history.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_hotkey_modes.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_ibus_engine_limits.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_ibus_engine_listener.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_ibus_pending.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_ibus_pids.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_ibus_ping.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_ibus_session.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_ibus_textlimit.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_ibus_typer.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_idle_audio.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_learn_clean.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_learn_import.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_learn_language.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_learn_metrics.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_learn_pipeline.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_learn_review.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_learn_targets.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_llm.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_llm_api.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_modifier_guard.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_numbers.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_paste_last.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_pipeline.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_platform.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_postcorrect.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_postprocess.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_prebuffer.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_prompt.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_quiet_audio.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_retention.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_robustness.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_security_hardening.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_server.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_setup.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_streaming.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_suggest.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_tokens.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_transcriber.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_tts.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_typer_manager.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_vad.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_vocabulary.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_wordfreq.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/tests/test_worker_decode.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/__main__.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/audio_source.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/backends.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/cli/__init__.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/cli/configcmd.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/cli/corrections.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/cli/doctor.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/cli/history.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/cli/models.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/cli/servicecmd.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/cli/uninstall.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/cli/vocab.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/clipboard_read.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/commands.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/consent.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/context_terms.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/core/__init__.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/core/bias.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/core/decoders/__init__.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/core/decoders/base.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/core/decoders/catalog.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/core/decoders/faster_whisper/__init__.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/core/decoders/lanes.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/core/decoders/sherpa.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/core/decoders/whispercpp.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/core/engine.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/core/pipeline.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/corrections.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/data/60-voiceio-uaccess.rules +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/data/__init__.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/demo.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/display.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/feedback.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/focus.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/health.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/hints.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/history.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/hotkeys/socket_backend.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/ibus/engine.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/ibus/install.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/ibus/pending.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/ibus/session.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/ibus/textlimit.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/idle_audio.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/learn/__init__.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/learn/augment.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/learn/clean.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/learn/importer.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/learn/metrics.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/learn/review.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/llm.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/llm_api.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/models/__init__.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/numbers.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/pidlock.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/platform.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/postcorrect.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/postprocess.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/power.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/privacy.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/prompt.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/quiet_audio.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/retention.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/server/__init__.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/server/__main__.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/server/app.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/server/config.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/server/runtime.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/setup/__init__.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/setup/configfile.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/setup/extras.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/setup/hotkey.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/setup/noninteractive.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/setup/speech.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/setup/system.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/setup/ui.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/streaming.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/suggest.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/tokens.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/tray/__init__.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/tray/_indicator.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/tray/_pystray.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/tts/base.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/tts/chain.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/tts/player.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/typers/base.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/typers/chain.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/typers/collecting.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/typers/fcitx_bridge.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/typers/ibus.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/typers/manager.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/typers/modifier_guard.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/typers/paste.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/typers/ydotool.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/ui.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/vad.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/vocab_stats.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/vocabulary.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/watchdog.py +0 -0
- {python_voiceio-1.9.2 → python_voiceio-1.9.3}/voiceio/wordfreq.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-voiceio
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.3
|
|
4
4
|
Summary: Voice dictation for Linux. Speak → text, locally, instantly.
|
|
5
5
|
Author: Hugo Montenegro
|
|
6
6
|
License-Expression: MIT
|
|
@@ -98,7 +98,7 @@ voiceio learn schedule on # learn from your recordings while the mac
|
|
|
98
98
|
voiceio learn status # what it learned and how each model scores
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
Scheduled learning runs daily or weekly at a time you pick, only
|
|
101
|
+
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).
|
|
102
102
|
|
|
103
103
|
## Configure
|
|
104
104
|
|
|
@@ -45,7 +45,7 @@ voiceio learn schedule on # learn from your recordings while the mac
|
|
|
45
45
|
voiceio learn status # what it learned and how each model scores
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
Scheduled learning runs daily or weekly at a time you pick, only
|
|
48
|
+
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).
|
|
49
49
|
|
|
50
50
|
## Configure
|
|
51
51
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-voiceio
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.3
|
|
4
4
|
Summary: Voice dictation for Linux. Speak → text, locally, instantly.
|
|
5
5
|
Author: Hugo Montenegro
|
|
6
6
|
License-Expression: MIT
|
|
@@ -98,7 +98,7 @@ voiceio learn schedule on # learn from your recordings while the mac
|
|
|
98
98
|
voiceio learn status # what it learned and how each model scores
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
Scheduled learning runs daily or weekly at a time you pick, only
|
|
101
|
+
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).
|
|
102
102
|
|
|
103
103
|
## Configure
|
|
104
104
|
|
|
@@ -20,6 +20,7 @@ tests/test_concurrency_lockdown.py
|
|
|
20
20
|
tests/test_config.py
|
|
21
21
|
tests/test_context_terms.py
|
|
22
22
|
tests/test_corrections.py
|
|
23
|
+
tests/test_dead_mic.py
|
|
23
24
|
tests/test_decoders.py
|
|
24
25
|
tests/test_engine.py
|
|
25
26
|
tests/test_evdev_uaccess.py
|
|
@@ -46,6 +47,7 @@ tests/test_learn_language.py
|
|
|
46
47
|
tests/test_learn_metrics.py
|
|
47
48
|
tests/test_learn_pipeline.py
|
|
48
49
|
tests/test_learn_quiet.py
|
|
50
|
+
tests/test_learn_resume.py
|
|
49
51
|
tests/test_learn_review.py
|
|
50
52
|
tests/test_learn_targets.py
|
|
51
53
|
tests/test_llm.py
|
|
@@ -58,6 +60,7 @@ tests/test_platform.py
|
|
|
58
60
|
tests/test_postcorrect.py
|
|
59
61
|
tests/test_postprocess.py
|
|
60
62
|
tests/test_prebuffer.py
|
|
63
|
+
tests/test_presence.py
|
|
61
64
|
tests/test_prompt.py
|
|
62
65
|
tests/test_quiet_audio.py
|
|
63
66
|
tests/test_recorder_integration.py
|
|
@@ -104,6 +107,7 @@ voiceio/platform.py
|
|
|
104
107
|
voiceio/postcorrect.py
|
|
105
108
|
voiceio/postprocess.py
|
|
106
109
|
voiceio/power.py
|
|
110
|
+
voiceio/presence.py
|
|
107
111
|
voiceio/privacy.py
|
|
108
112
|
voiceio/prompt.py
|
|
109
113
|
voiceio/quiet_audio.py
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""A mic stream that dies mid-recording is reopened, not recorded to the end.
|
|
2
|
+
|
|
3
|
+
The real failure: after a suspend/resume the stream delivered 2 s of audio,
|
|
4
|
+
then 102 s of exact zeros; the whole dictation was lost.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import threading
|
|
9
|
+
from unittest.mock import MagicMock, patch
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from voiceio.config import AudioConfig
|
|
14
|
+
from voiceio.recorder import AudioRecorder
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _recorder() -> AudioRecorder:
|
|
18
|
+
rec = AudioRecorder(AudioConfig(sample_rate=16000, prebuffer_secs=0.5,
|
|
19
|
+
auto_stop_silence_secs=0, auto_stop_no_speech_secs=0))
|
|
20
|
+
rec._stream = MagicMock()
|
|
21
|
+
return rec
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _feed(rec: AudioRecorder, value: float, secs: float) -> None:
|
|
25
|
+
for _ in range(int(secs * 16000 / 1600)):
|
|
26
|
+
rec._callback(np.full((1600, 1), value, dtype=np.float32), 1600, None, None)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _armed(rec: AudioRecorder) -> threading.Event:
|
|
30
|
+
fired = threading.Event()
|
|
31
|
+
rec.set_on_dead_stream(fired.set)
|
|
32
|
+
return fired
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_zeros_after_real_audio_fire_once():
|
|
36
|
+
rec = _recorder()
|
|
37
|
+
rec.start()
|
|
38
|
+
fired = _armed(rec)
|
|
39
|
+
_feed(rec, 0.02, 1.0)
|
|
40
|
+
_feed(rec, 0.0, 1.2)
|
|
41
|
+
assert fired.wait(2)
|
|
42
|
+
fired.clear()
|
|
43
|
+
_feed(rec, 0.0, 1.2) # still reviving: not fired again
|
|
44
|
+
assert not fired.wait(0.2)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_a_mic_silent_from_the_start_is_left_to_the_muted_check():
|
|
48
|
+
rec = _recorder()
|
|
49
|
+
rec.start()
|
|
50
|
+
fired = _armed(rec)
|
|
51
|
+
_feed(rec, 0.0, 2.0)
|
|
52
|
+
assert not fired.wait(0.2)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_quiet_room_noise_is_not_a_dead_stream():
|
|
56
|
+
rec = _recorder()
|
|
57
|
+
rec.start()
|
|
58
|
+
fired = _armed(rec)
|
|
59
|
+
_feed(rec, 0.02, 0.5)
|
|
60
|
+
_feed(rec, 0.0005, 3.0) # a working mic's silence is never exact zero
|
|
61
|
+
assert not fired.wait(0.2)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_revive_reopens_and_allows_at_most_two_attempts():
|
|
65
|
+
rec = _recorder()
|
|
66
|
+
rec.start()
|
|
67
|
+
with patch.object(rec, "open_stream") as opened, patch.object(rec, "close_stream"):
|
|
68
|
+
for expected in (True, True, False):
|
|
69
|
+
fired = _armed(rec)
|
|
70
|
+
_feed(rec, 0.02, 0.2)
|
|
71
|
+
_feed(rec, 0.0, 1.2)
|
|
72
|
+
assert fired.wait(0.5) is expected
|
|
73
|
+
if expected:
|
|
74
|
+
rec.revive_stream()
|
|
75
|
+
assert opened.call_count == 2
|
|
76
|
+
assert rec.is_recording
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_daemon_revives_and_warns_only_when_still_dead():
|
|
80
|
+
from voiceio.app import VoiceIO, _State
|
|
81
|
+
vio = VoiceIO.__new__(VoiceIO)
|
|
82
|
+
vio._hotkey_lock = threading.Lock()
|
|
83
|
+
vio._state = _State.RECORDING
|
|
84
|
+
vio._recording = 7
|
|
85
|
+
vio.recorder = MagicMock(silent_signal_secs=0.0)
|
|
86
|
+
with patch("voiceio.app.time.sleep"), patch("voiceio.feedback.notify") as notify:
|
|
87
|
+
vio._revive_mic(7)
|
|
88
|
+
vio.recorder.revive_stream.assert_called_once()
|
|
89
|
+
notify.assert_not_called()
|
|
90
|
+
vio.recorder.silent_signal_secs = 1.5
|
|
91
|
+
vio._revive_mic(7)
|
|
92
|
+
notify.assert_called_once()
|
|
93
|
+
vio._revive_mic(8) # a later recording: not ours to touch
|
|
94
|
+
assert vio.recorder.revive_stream.call_count == 2
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from voiceio import power
|
|
6
|
+
from voiceio.learn import quiet
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _supply(root, name, kind, online):
|
|
10
|
+
d = root / name
|
|
11
|
+
d.mkdir()
|
|
12
|
+
(d / "type").write_text(kind + "\n")
|
|
13
|
+
(d / "online").write_text(online + "\n")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_on_battery_needs_a_mains_supply_that_is_offline(tmp_path):
|
|
17
|
+
assert not power.on_battery(tmp_path) # desktop: no supplies listed
|
|
18
|
+
_supply(tmp_path, "BAT0", "Battery", "0")
|
|
19
|
+
_supply(tmp_path, "AC", "Mains", "0")
|
|
20
|
+
assert power.on_battery(tmp_path)
|
|
21
|
+
(tmp_path / "AC" / "online").write_text("1\n")
|
|
22
|
+
assert not power.on_battery(tmp_path)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_busy_reason_is_load_per_cpu_then_battery(monkeypatch):
|
|
26
|
+
monkeypatch.setattr(quiet.os, "cpu_count", lambda: 8)
|
|
27
|
+
monkeypatch.setattr(quiet, "on_battery", lambda: False)
|
|
28
|
+
monkeypatch.setattr(quiet.os, "getloadavg", lambda: (4.0, 0, 0)) # 0.5 per CPU
|
|
29
|
+
assert "load 0.50 per CPU" in quiet.busy_reason(0.25)
|
|
30
|
+
monkeypatch.setattr(quiet.os, "getloadavg", lambda: (1.0, 0, 0))
|
|
31
|
+
assert quiet.busy_reason(0.25) is None
|
|
32
|
+
monkeypatch.setattr(quiet, "on_battery", lambda: True)
|
|
33
|
+
assert quiet.busy_reason(0.25) == "on battery"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_wait_until_quiet_waits_then_gives_up(monkeypatch):
|
|
37
|
+
now = [0.0]
|
|
38
|
+
sleeps = []
|
|
39
|
+
reasons = iter(["busy", "busy", None])
|
|
40
|
+
monkeypatch.setattr(quiet, "busy_reason", lambda *_: next(reasons))
|
|
41
|
+
args = dict(poll=300, sleep=lambda s: (sleeps.append(s), now.__setitem__(0, now[0] + s)),
|
|
42
|
+
clock=lambda: now[0], say=lambda _: None)
|
|
43
|
+
assert quiet.wait_until_quiet(0.25, 3, **args)
|
|
44
|
+
assert sleeps == [300, 300]
|
|
45
|
+
|
|
46
|
+
monkeypatch.setattr(quiet, "busy_reason", lambda *_: "on battery")
|
|
47
|
+
said = []
|
|
48
|
+
now[0] = 0.0
|
|
49
|
+
assert not quiet.wait_until_quiet(0.25, 1, **{**args, "say": said.append})
|
|
50
|
+
assert now[0] <= 3600 and said[-1].startswith("still on battery after 1 h")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_busy_reason_waits_for_the_user_to_be_away(monkeypatch):
|
|
54
|
+
from voiceio import presence
|
|
55
|
+
monkeypatch.setattr(quiet, "on_battery", lambda: False)
|
|
56
|
+
monkeypatch.setattr(quiet.os, "getloadavg", lambda: (0.0, 0, 0))
|
|
57
|
+
seen = []
|
|
58
|
+
monkeypatch.setattr(presence, "check",
|
|
59
|
+
lambda away: seen.append(away) or presence.Presence(True, "in use (x)"))
|
|
60
|
+
assert quiet.busy_reason(0.25, away_after=600) == "in use (x)" and seen == [600]
|
|
61
|
+
assert quiet.busy_reason(0.25) is None # unattended `maintain`: no presence check
|
|
62
|
+
monkeypatch.setattr(presence, "check", lambda away: presence.Presence(False, "away"))
|
|
63
|
+
assert quiet.busy_reason(0.25, away_after=600) is None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_waiting_logs_each_reason_once_not_every_reading(monkeypatch):
|
|
67
|
+
readings = iter(["in use (last input 3 s ago)", "in use (last input 9 s ago)",
|
|
68
|
+
"on battery", None])
|
|
69
|
+
monkeypatch.setattr(quiet, "busy_reason", lambda *_: next(readings))
|
|
70
|
+
said = []
|
|
71
|
+
assert quiet.wait_until_quiet(0.25, 3, poll=1, sleep=lambda s: None, say=said.append)
|
|
72
|
+
assert said == ["waiting: in use (last input 3 s ago)", "waiting: on battery"]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class _Proc:
|
|
76
|
+
"""A worker that runs for `polls` checks, then exits with `code`."""
|
|
77
|
+
def __init__(self, polls=99, code=0):
|
|
78
|
+
self.left, self.code, self.pid, self.stopped = polls, code, 4242, False
|
|
79
|
+
|
|
80
|
+
def poll(self):
|
|
81
|
+
if self.stopped:
|
|
82
|
+
return -15
|
|
83
|
+
if self.left <= 0:
|
|
84
|
+
return self.code
|
|
85
|
+
self.left -= 1
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def returncode(self):
|
|
90
|
+
return self.poll()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _watch(monkeypatch, tmp_path, procs, reasons, *, quiet_ok=True):
|
|
94
|
+
spawned, said = [], []
|
|
95
|
+
procs = iter(procs)
|
|
96
|
+
reasons = iter(reasons)
|
|
97
|
+
|
|
98
|
+
def spawn(argv, env):
|
|
99
|
+
spawned.append(env)
|
|
100
|
+
return next(procs)
|
|
101
|
+
|
|
102
|
+
def stop(proc):
|
|
103
|
+
proc.stopped = True
|
|
104
|
+
monkeypatch.setattr(quiet, "wait_until_quiet", lambda *a, **k: quiet_ok)
|
|
105
|
+
monkeypatch.setattr(quiet, "stop_reason", lambda away: next(reasons, None))
|
|
106
|
+
code = quiet.run_while_away(["voiceio", "learn", "maintain"], max_load=0.25, away_minutes=10,
|
|
107
|
+
wait_hours=3, threads=3, hold_file=tmp_path / "hold",
|
|
108
|
+
spawn=spawn, stop=stop, sleep=lambda s: None, say=said.append)
|
|
109
|
+
return code, spawned, said
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_work_stops_when_the_user_returns_and_resumes_later(monkeypatch, tmp_path):
|
|
113
|
+
first, second = _Proc(), _Proc(polls=3, code=0)
|
|
114
|
+
code, spawned, said = _watch(monkeypatch, tmp_path, [first, second],
|
|
115
|
+
[None, None, "in use (last input 0 s ago)"])
|
|
116
|
+
assert code == 0 and first.stopped and not second.stopped
|
|
117
|
+
assert said == ["paused: in use (last input 0 s ago); it resumes where it stopped "
|
|
118
|
+
"once you've been away 10 min"]
|
|
119
|
+
assert spawned[0][quiet.WORKER_ENV] == "1" and spawned[0]["OMP_NUM_THREADS"] == "3"
|
|
120
|
+
assert quiet.RESUMED_ENV not in spawned[0] and spawned[1][quiet.RESUMED_ENV] == "1"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_watcher_reports_the_work_exit_status_and_gives_up_quietly(monkeypatch, tmp_path):
|
|
124
|
+
assert _watch(monkeypatch, tmp_path, [_Proc(polls=1, code=3)], [])[0] == 3
|
|
125
|
+
code, spawned, _ = _watch(monkeypatch, tmp_path, [], [], quiet_ok=False)
|
|
126
|
+
assert code == 0 and spawned == [] # never away: nothing started, unit doesn't fail
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_a_model_switch_in_progress_is_never_cut_off(monkeypatch, tmp_path):
|
|
130
|
+
proc = _Proc(polls=3, code=0)
|
|
131
|
+
reasons = iter(["in use"] * 10)
|
|
132
|
+
|
|
133
|
+
def spawn(argv, env):
|
|
134
|
+
(tmp_path / "hold").touch() # the worker holds it
|
|
135
|
+
return proc
|
|
136
|
+
monkeypatch.setattr(quiet, "wait_until_quiet", lambda *a, **k: True)
|
|
137
|
+
monkeypatch.setattr(quiet, "stop_reason", lambda away: next(reasons))
|
|
138
|
+
assert quiet.run_while_away(["x"], max_load=1, away_minutes=1, wait_hours=1, threads=1,
|
|
139
|
+
hold_file=tmp_path / "hold", spawn=spawn,
|
|
140
|
+
stop=lambda p: pytest.fail("cut off a model switch"),
|
|
141
|
+
sleep=lambda s: None, say=print) == 0
|
|
142
|
+
assert not (tmp_path / "hold").exists()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_hold_marks_the_file_only_inside_the_block(monkeypatch, tmp_path):
|
|
146
|
+
flag = tmp_path / "hold"
|
|
147
|
+
monkeypatch.setenv(quiet.HOLD_ENV, str(flag))
|
|
148
|
+
with quiet.hold():
|
|
149
|
+
assert flag.exists()
|
|
150
|
+
assert not flag.exists()
|
|
151
|
+
monkeypatch.delenv(quiet.HOLD_ENV)
|
|
152
|
+
with quiet.hold(): # not under the watcher: a no-op
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_stop_ends_the_whole_process_group_at_once():
|
|
157
|
+
"""The decoder runs in a subprocess of the worker: it must go too."""
|
|
158
|
+
import os
|
|
159
|
+
import time
|
|
160
|
+
proc = quiet._spawn(["sh", "-c", "sleep 30 & sleep 30; wait"], dict(os.environ))
|
|
161
|
+
time.sleep(0.2)
|
|
162
|
+
t0 = time.monotonic()
|
|
163
|
+
quiet._stop(proc)
|
|
164
|
+
assert time.monotonic() - t0 < 3 and proc.poll() is not None
|
|
165
|
+
with pytest.raises(ProcessLookupError):
|
|
166
|
+
os.killpg(proc.pid, 0) # nothing left in the group
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_threads_default_to_half_the_cpus(monkeypatch):
|
|
170
|
+
monkeypatch.setattr(quiet.os, "cpu_count", lambda: 16)
|
|
171
|
+
assert quiet.worker_threads(0) == 8 and quiet.worker_threads(5) == 5
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_maintain_in_the_worker_does_the_work(monkeypatch):
|
|
175
|
+
from voiceio.cli import learn as cli
|
|
176
|
+
monkeypatch.setattr(cli, "_yield_cpu", lambda: None)
|
|
177
|
+
monkeypatch.setenv(quiet.WORKER_ENV, "1")
|
|
178
|
+
monkeypatch.setattr(quiet, "run_while_away", lambda *a, **k: pytest.fail("worker must not watch"))
|
|
179
|
+
ran = []
|
|
180
|
+
monkeypatch.setattr(cli, "_maintain", lambda args, cfg: ran.append(cfg.away_minutes))
|
|
181
|
+
cli.cmd_maintain(argparse.Namespace(min_new=5, base="small", epochs=1, when_idle=True))
|
|
182
|
+
assert ran == [10.0]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_scheduled_maintain_fails_when_its_work_does(monkeypatch):
|
|
186
|
+
from voiceio.cli import learn as cli
|
|
187
|
+
monkeypatch.setattr(cli, "_yield_cpu", lambda: None)
|
|
188
|
+
monkeypatch.delenv(quiet.WORKER_ENV, raising=False)
|
|
189
|
+
monkeypatch.setattr(quiet, "run_while_away", lambda *a, **k: 2)
|
|
190
|
+
with pytest.raises(SystemExit) as e:
|
|
191
|
+
cli.cmd_maintain(argparse.Namespace(min_new=5, base="small", epochs=1, when_idle=True))
|
|
192
|
+
assert e.value.code == 2
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_scheduled_maintain_waits_for_a_quiet_machine(monkeypatch):
|
|
196
|
+
from voiceio.cli import learn as cli
|
|
197
|
+
monkeypatch.setattr(cli, "_yield_cpu", lambda: None)
|
|
198
|
+
monkeypatch.setattr(quiet, "wait_until_quiet", lambda *a, **k: False)
|
|
199
|
+
ran = []
|
|
200
|
+
monkeypatch.setattr(cli, "cmd_label", lambda a: ran.append("label"))
|
|
201
|
+
cli.cmd_maintain(argparse.Namespace(min_new=5, base="small", epochs=1, when_idle=True))
|
|
202
|
+
assert ran == [] # busy all night: nothing ran
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_learning_announcement_offers_stop_and_weekly(monkeypatch, tmp_path):
|
|
206
|
+
from voiceio import feedback
|
|
207
|
+
from voiceio.cli import learn as cli
|
|
208
|
+
sent = []
|
|
209
|
+
monkeypatch.setattr(feedback, "notify_actions", lambda *a: sent.append(a))
|
|
210
|
+
monkeypatch.setattr(cli.store, "load_labels", lambda: {})
|
|
211
|
+
monkeypatch.setattr(cli.store, "clips", lambda: [argparse.Namespace(audio=f"a{i}.wav") for i in range(3)])
|
|
212
|
+
monkeypatch.setattr(cli.store, "latest_dataset", lambda: None)
|
|
213
|
+
cli._announce_learning(min_new=50)
|
|
214
|
+
(title, body, actions, _), = sent
|
|
215
|
+
assert "learning" in title and body.startswith("Labeling 3 new recordings,")
|
|
216
|
+
assert "while you're away" in body and set(actions) == {"stop", "weekly"}
|
|
217
|
+
|
|
218
|
+
sent.clear()
|
|
219
|
+
monkeypatch.setattr(cli.store, "load_labels", lambda: {f"a{i}.wav": 1 for i in range(3)})
|
|
220
|
+
monkeypatch.setattr(cli.store, "latest_dataset", lambda: tmp_path)
|
|
221
|
+
monkeypatch.setattr(cli.store, "dataset_clips", lambda _: {f"a{i}.wav" for i in range(3)})
|
|
222
|
+
cli._announce_learning(min_new=50)
|
|
223
|
+
assert sent == [] # nothing to do: no notification
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_notify_actions_runs_the_picked_action(monkeypatch):
|
|
227
|
+
import subprocess
|
|
228
|
+
|
|
229
|
+
from voiceio import feedback
|
|
230
|
+
monkeypatch.setattr("sys.platform", "linux")
|
|
231
|
+
monkeypatch.setattr(feedback, "_which", lambda _: "/usr/bin/notify-send")
|
|
232
|
+
monkeypatch.setattr(feedback, "_notify_send_has_actions", lambda: True)
|
|
233
|
+
calls = []
|
|
234
|
+
|
|
235
|
+
def fake_run(args, **kw):
|
|
236
|
+
calls.append(args)
|
|
237
|
+
return subprocess.CompletedProcess(args, 0, stdout="weekly\n")
|
|
238
|
+
monkeypatch.setattr(feedback.subprocess, "run", fake_run)
|
|
239
|
+
picked = []
|
|
240
|
+
|
|
241
|
+
class Now: # run the waiting thread inline
|
|
242
|
+
def __init__(self, target, daemon): self.target = target
|
|
243
|
+
def start(self): self.target()
|
|
244
|
+
monkeypatch.setattr(feedback.threading, "Thread", Now)
|
|
245
|
+
feedback.notify_actions("t", "b", {"stop": "Stop", "weekly": "Weekly"}, picked.append)
|
|
246
|
+
assert picked == ["weekly"]
|
|
247
|
+
assert "--wait" in calls[0] and "--action=stop=Stop" in calls[0]
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""A learning run that is stopped part-way carries on where it was."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import wave
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from voiceio import config
|
|
13
|
+
from voiceio.cli import learn as cli
|
|
14
|
+
from voiceio.learn import evaluate, store, train
|
|
15
|
+
from voiceio.learn.store import Segment
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _clip(name: str) -> None:
|
|
19
|
+
config.RECORDINGS_DIR.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
with wave.open(str(config.RECORDINGS_DIR / name), "wb") as w:
|
|
21
|
+
w.setnchannels(1)
|
|
22
|
+
w.setsampwidth(2)
|
|
23
|
+
w.setframerate(16000)
|
|
24
|
+
w.writeframes(np.zeros(3 * 16000, dtype=np.int16).tobytes())
|
|
25
|
+
config.HISTORY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
with open(config.HISTORY_PATH, "a") as f:
|
|
27
|
+
f.write(json.dumps({"ts": 1.0, "text": "x", "raw": "x", "duration": 3,
|
|
28
|
+
"audio": name}) + "\n")
|
|
29
|
+
store.add_label(name, [Segment(0, 3, "hello there")], "teacher:fake")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Stopped(Exception):
|
|
33
|
+
"""Stands in for the watcher ending the worker mid-stage."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _fake_work(monkeypatch, train_fn):
|
|
37
|
+
evals, trained = [], []
|
|
38
|
+
monkeypatch.setattr(cli, "cmd_label", lambda a: None)
|
|
39
|
+
monkeypatch.setattr(cli, "_announce_learning", lambda min_new: None)
|
|
40
|
+
monkeypatch.setattr(train, "missing_train_deps", lambda: [])
|
|
41
|
+
|
|
42
|
+
def run_eval(model, ds):
|
|
43
|
+
evals.append(model)
|
|
44
|
+
return SimpleNamespace(wer=0.1, term_recall=0.5, repetitions=0, per_clip=[])
|
|
45
|
+
monkeypatch.setattr(cli, "_run_eval", run_eval)
|
|
46
|
+
monkeypatch.setattr(cli, "_train", lambda ds, **k: trained.append(ds.name) or train_fn(ds))
|
|
47
|
+
monkeypatch.setattr(evaluate, "promotion_verdict", lambda c, b: (False, "table"))
|
|
48
|
+
return evals, trained
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_a_stopped_retrain_carries_on_instead_of_waiting_for_more_clips(monkeypatch, capsys):
|
|
52
|
+
for i in range(3):
|
|
53
|
+
_clip(f"{i}.wav")
|
|
54
|
+
run = store.learn_dir() / "models" / "v0001-small-20261019-0822"
|
|
55
|
+
|
|
56
|
+
def stopped(ds):
|
|
57
|
+
raise Stopped
|
|
58
|
+
evals, trained = _fake_work(monkeypatch, stopped)
|
|
59
|
+
with pytest.raises(Stopped):
|
|
60
|
+
cli.cmd_maintain(argparse.Namespace(min_new=3, base="small", epochs=1))
|
|
61
|
+
assert trained == ["v0001"]
|
|
62
|
+
|
|
63
|
+
# Before: v0001 counted as done, so it waited for 3 more clips past it.
|
|
64
|
+
evals, trained = _fake_work(monkeypatch, lambda ds: run)
|
|
65
|
+
cli.cmd_maintain(argparse.Namespace(min_new=3, base="small", epochs=1))
|
|
66
|
+
out = capsys.readouterr().out
|
|
67
|
+
assert "carrying on with v0001" in out and trained == ["v0001"]
|
|
68
|
+
assert store.latest_dataset().name == "v0001" # no second dataset built
|
|
69
|
+
|
|
70
|
+
cli.cmd_maintain(argparse.Namespace(min_new=3, base="small", epochs=1))
|
|
71
|
+
assert "retraining waits for 3" in capsys.readouterr().out # decided: done with it
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_resume_reuses_saved_scores_and_a_finished_model(monkeypatch):
|
|
75
|
+
for i in range(3):
|
|
76
|
+
_clip(f"{i}.wav")
|
|
77
|
+
ds = store.build_dataset()
|
|
78
|
+
cli._set_pending(ds)
|
|
79
|
+
run = store.learn_dir() / "models" / f"{ds.name}-small-20261019-0822"
|
|
80
|
+
(run / "ct2").mkdir(parents=True)
|
|
81
|
+
(run / "ct2" / "model.bin").write_text("")
|
|
82
|
+
current = config.load().model.name
|
|
83
|
+
saved = {current: {"wer": 0.2, "term_recall": 0.5, "repetitions": 0, "per_clip": []}}
|
|
84
|
+
monkeypatch.setattr(evaluate, "latest", lambda model, dataset, beam=5: saved.get(model))
|
|
85
|
+
evals, trained = _fake_work(monkeypatch, lambda ds: pytest.fail("already trained"))
|
|
86
|
+
cli.cmd_maintain(argparse.Namespace(min_new=50, base="small", epochs=1))
|
|
87
|
+
assert evals == [str(run / "ct2")] # only the candidate was still unscored
|
|
88
|
+
assert cli._pending_dataset() is None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_pending_marker_survives_only_with_its_dataset():
|
|
92
|
+
for i in range(2):
|
|
93
|
+
_clip(f"{i}.wav")
|
|
94
|
+
ds = store.build_dataset()
|
|
95
|
+
cli._set_pending(ds)
|
|
96
|
+
assert cli._pending_dataset() == ds
|
|
97
|
+
cli._set_pending(None)
|
|
98
|
+
assert cli._pending_dataset() is None
|
|
99
|
+
(store.learn_dir() / "maintain-pending.json").write_text('{"dataset": "v0099"}')
|
|
100
|
+
assert cli._pending_dataset() is None and not cli._pending_path().exists()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_a_half_built_dataset_is_never_the_latest(monkeypatch):
|
|
104
|
+
_clip("a.wav")
|
|
105
|
+
real = store.json.dumps
|
|
106
|
+
calls = []
|
|
107
|
+
|
|
108
|
+
def dies_at_manifest(obj, *a, **k):
|
|
109
|
+
if isinstance(obj, dict) and "schema" in obj:
|
|
110
|
+
raise Stopped
|
|
111
|
+
calls.append(1)
|
|
112
|
+
return real(obj, *a, **k)
|
|
113
|
+
monkeypatch.setattr(store.json, "dumps", dies_at_manifest)
|
|
114
|
+
with pytest.raises(Stopped):
|
|
115
|
+
store.build_dataset()
|
|
116
|
+
monkeypatch.setattr(store.json, "dumps", real)
|
|
117
|
+
assert store.latest_dataset() is None
|
|
118
|
+
assert store.build_dataset().name == "v0001"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_train_resumes_the_unfinished_run_of_the_same_dataset_and_base(monkeypatch):
|
|
122
|
+
for i in range(2):
|
|
123
|
+
_clip(f"{i}.wav")
|
|
124
|
+
ds = store.build_dataset()
|
|
125
|
+
folder = store.learn_dir() / "exports" / ds.name
|
|
126
|
+
folder.mkdir(parents=True)
|
|
127
|
+
for name in ("train.jsonl", "dev.jsonl", train.HOTWORDS_FILE):
|
|
128
|
+
(folder / name).write_text("")
|
|
129
|
+
models = store.learn_dir() / "models"
|
|
130
|
+
old = models / f"{ds.name}-small-20261018-0330" # finished before
|
|
131
|
+
(old / "ct2").mkdir(parents=True)
|
|
132
|
+
(old / "ct2" / "model.bin").write_text("")
|
|
133
|
+
stopped = models / f"{ds.name}-small-20261019-0822" # stopped mid-training
|
|
134
|
+
stopped.mkdir()
|
|
135
|
+
(stopped / train.CHECKPOINT).write_text("x")
|
|
136
|
+
(models / f"{ds.name}-small.en-20261019-0900").mkdir() # another base: not ours
|
|
137
|
+
got = {}
|
|
138
|
+
monkeypatch.setattr(train, "require_train_deps", lambda: None)
|
|
139
|
+
monkeypatch.setattr(train, "export", lambda *a, **k: pytest.fail("re-exported"))
|
|
140
|
+
monkeypatch.setattr(train, "train", lambda folder, out, cfg: got.update(out=out, cfg=cfg)
|
|
141
|
+
or out / "hf")
|
|
142
|
+
|
|
143
|
+
def convert(hf, out):
|
|
144
|
+
out.mkdir()
|
|
145
|
+
(out / "model.bin").write_text("")
|
|
146
|
+
monkeypatch.setattr(train, "convert", convert)
|
|
147
|
+
config.set_value("learn", "threads", 3)
|
|
148
|
+
assert cli._train(ds, base="small", epochs=1) == stopped and got["out"] == stopped
|
|
149
|
+
assert got["cfg"].threads == 3
|
|
150
|
+
assert not (stopped / train.CHECKPOINT).exists() # finished: resume state dropped
|
|
151
|
+
assert cli._finished_run(ds, "small") == stopped
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ── the training loop itself ─────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
def _rows(n=10):
|
|
157
|
+
rng = np.random.default_rng(7)
|
|
158
|
+
return [{"audio": f"w{i}", "x": rng.normal(size=3).tolist(), "y": float(rng.normal())}
|
|
159
|
+
for i in range(n)]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _fit(tmp_path, *, stop_at=None, checkpoint_secs=0.0, fp="fp"):
|
|
163
|
+
"""fit() on a tiny model whose forward draws from mix_rng and dropout, so
|
|
164
|
+
every random state a resume must restore is in play."""
|
|
165
|
+
torch = pytest.importorskip("torch")
|
|
166
|
+
torch.manual_seed(0)
|
|
167
|
+
model = torch.nn.Sequential(torch.nn.Linear(3, 8), torch.nn.Dropout(0.3), torch.nn.Linear(8, 1))
|
|
168
|
+
mix_rng = np.random.default_rng(1)
|
|
169
|
+
calls = [0]
|
|
170
|
+
|
|
171
|
+
def forward(chunk, *, fixed=False):
|
|
172
|
+
calls[0] += 1
|
|
173
|
+
if stop_at is not None and calls[0] == stop_at:
|
|
174
|
+
raise Stopped
|
|
175
|
+
x = torch.tensor([r["x"] for r in chunk])
|
|
176
|
+
noise = 0.0 if fixed else float(mix_rng.random())
|
|
177
|
+
y = torch.tensor([[r["y"] + noise] for r in chunk])
|
|
178
|
+
return torch.nn.functional.mse_loss(model(x), y)
|
|
179
|
+
cfg = train.TrainConfig(epochs=3, batch_size=3, lr=1e-2)
|
|
180
|
+
rows = _rows()
|
|
181
|
+
best = train.fit(model, forward, rows[:7], rows[7:], cfg, checkpoint=tmp_path / "ck.pt",
|
|
182
|
+
fp=fp, order_rng=np.random.default_rng(0), mix_rng=mix_rng,
|
|
183
|
+
save_best=lambda: None, checkpoint_secs=checkpoint_secs)
|
|
184
|
+
return model, best, calls[0]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@pytest.mark.parametrize("stop_at", [2, 4, 6, 9]) # mid-epoch, at a dev pass, epoch 2, 3
|
|
188
|
+
def test_a_resumed_run_takes_exactly_the_steps_of_an_uninterrupted_one(tmp_path, stop_at):
|
|
189
|
+
torch = pytest.importorskip("torch")
|
|
190
|
+
(tmp_path / "a").mkdir()
|
|
191
|
+
whole, best_whole, calls_whole = _fit(tmp_path / "a")
|
|
192
|
+
assert calls_whole == 12 # 3 epochs × (3 train steps + 1 dev pass)
|
|
193
|
+
(tmp_path / "b").mkdir()
|
|
194
|
+
with pytest.raises(Stopped):
|
|
195
|
+
_fit(tmp_path / "b", stop_at=stop_at)
|
|
196
|
+
resumed, best_resumed, calls_resumed = _fit(tmp_path / "b")
|
|
197
|
+
# It carried on (at most the step in flight is redone), and ended where
|
|
198
|
+
# the uninterrupted run did, to the last bit of every random draw.
|
|
199
|
+
assert calls_resumed <= calls_whole - (stop_at - 1) + 1
|
|
200
|
+
for p, q in zip(whole.parameters(), resumed.parameters()):
|
|
201
|
+
assert torch.allclose(p, q, atol=1e-6)
|
|
202
|
+
assert best_resumed == pytest.approx(best_whole)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_progress_for_other_data_or_settings_starts_over(tmp_path, capsys):
|
|
206
|
+
(tmp_path / "a").mkdir()
|
|
207
|
+
with pytest.raises(Stopped):
|
|
208
|
+
_fit(tmp_path / "a", stop_at=4)
|
|
209
|
+
assert _fit(tmp_path / "a", fp="other")[2] == 12 # all of it again
|
|
210
|
+
assert "starting over" in capsys.readouterr().out
|
|
211
|
+
(tmp_path / "a" / "ck.pt").write_text("not a checkpoint")
|
|
212
|
+
_fit(tmp_path / "a") # unreadable: starts over, no crash
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_fingerprint_follows_data_and_settings_not_threads(tmp_path):
|
|
216
|
+
for name in ("train.jsonl", "dev.jsonl"):
|
|
217
|
+
(tmp_path / name).write_text('{"audio": "a"}\n')
|
|
218
|
+
fp = train.fingerprint(tmp_path, train.TrainConfig())
|
|
219
|
+
assert train.fingerprint(tmp_path, train.TrainConfig(threads=3)) == fp
|
|
220
|
+
assert train.fingerprint(tmp_path, train.TrainConfig(epochs=9)) != fp
|
|
221
|
+
assert train.fingerprint(tmp_path, train.TrainConfig(), device="cuda") != fp
|
|
222
|
+
(tmp_path / "train.jsonl").write_text('{"audio": "b"}\n')
|
|
223
|
+
assert train.fingerprint(tmp_path, train.TrainConfig()) != fp
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_encoder_cache_persists_across_runs_only_for_the_same_data(tmp_path):
|
|
227
|
+
keys = ["train/0.wav", "train/1.wav", "dev/0.wav"]
|
|
228
|
+
cache = train.EncoderCache(tmp_path / train.ENCODER_CACHE, keys, frames=4, width=2, fingerprint="a")
|
|
229
|
+
assert cache.missing(keys) == keys and cache.reused == 0
|
|
230
|
+
value = np.arange(8, dtype=np.float16).reshape(1, 4, 2)
|
|
231
|
+
cache.put(["train/1.wav"], value)
|
|
232
|
+
del cache
|
|
233
|
+
|
|
234
|
+
again = train.EncoderCache(tmp_path / train.ENCODER_CACHE, keys, frames=4, width=2, fingerprint="a")
|
|
235
|
+
assert again.reused == 1 and again.missing(keys) == ["train/0.wav", "dev/0.wav"]
|
|
236
|
+
np.testing.assert_array_equal(again.get(["train/1.wav"]), value)
|
|
237
|
+
del again
|
|
238
|
+
|
|
239
|
+
other = train.EncoderCache(tmp_path / train.ENCODER_CACHE, keys, frames=4, width=2, fingerprint="b")
|
|
240
|
+
assert other.missing(keys) == keys
|
|
241
|
+
del other
|
|
242
|
+
train.discard_resume_state(tmp_path)
|
|
243
|
+
assert not list(tmp_path.glob("encoder-cache*"))
|