python-voiceio 1.2.0__tar.gz → 1.4.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.2.0/python_voiceio.egg-info → python_voiceio-1.4.0}/PKG-INFO +13 -7
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/README.md +11 -5
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/pyproject.toml +2 -2
- {python_voiceio-1.2.0 → python_voiceio-1.4.0/python_voiceio.egg-info}/PKG-INFO +13 -7
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/python_voiceio.egg-info/SOURCES.txt +2 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_health.py +1 -1
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_learn_pipeline.py +78 -3
- python_voiceio-1.4.0/tests/test_learn_quiet.py +102 -0
- python_voiceio-1.4.0/tests/test_learn_review.py +44 -0
- python_voiceio-1.4.0/tests/test_service.py +116 -0
- python_voiceio-1.4.0/voiceio/__init__.py +1 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/app.py +8 -1
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/cli/learn.py +151 -19
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/cli/servicecmd.py +29 -17
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/config.py +19 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/feedback.py +34 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/health.py +2 -2
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/learn/evaluate.py +15 -5
- python_voiceio-1.4.0/voiceio/learn/quiet.py +54 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/learn/review.py +30 -9
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/pidlock.py +18 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/service.py +65 -11
- python_voiceio-1.2.0/tests/test_learn_review.py +0 -20
- python_voiceio-1.2.0/tests/test_service.py +0 -62
- python_voiceio-1.2.0/voiceio/__init__.py +0 -1
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/LICENSE +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/python_voiceio.egg-info/requires.txt +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/setup.cfg +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_app_wiring.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_audio_quality.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_audio_source.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_backend_probes.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_cli.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_clipboard_read.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_commands.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_concurrency_lockdown.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_config.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_corrections.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_decoders.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_engine.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_evdev_uaccess.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_fallback.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_fcitx_bridge.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_hints.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_history.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_ibus_engine_limits.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_ibus_engine_listener.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_ibus_pending.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_ibus_pids.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_ibus_ping.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_ibus_session.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_ibus_textlimit.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_ibus_typer.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_learn_clean.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_learn_import.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_learn_metrics.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_learn_targets.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_llm.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_llm_api.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_numbers.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_pipeline.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_platform.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_postcorrect.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_postprocess.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_prebuffer.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_prompt.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_retention.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_robustness.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_security_hardening.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_server.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_setup.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_streaming.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_suggest.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_tokens.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_transcriber.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_tts.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_typer_manager.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_vad.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_vocabulary.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_wordfreq.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/tests/test_worker_decode.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/__main__.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/audio_source.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/backends.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/cli/__init__.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/cli/configcmd.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/cli/corrections.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/cli/doctor.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/cli/history.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/cli/models.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/cli/uninstall.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/cli/vocab.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/clipboard_read.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/commands.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/consent.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/core/__init__.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/core/bias.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/core/decoders/__init__.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/core/decoders/base.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/core/decoders/catalog.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/core/decoders/faster_whisper/__init__.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/core/decoders/lanes.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/core/decoders/sherpa.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/core/decoders/whispercpp.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/core/engine.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/core/pipeline.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/corrections.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/data/__init__.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/demo.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/display.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/hints.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/history.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/hotkeys/socket_backend.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/ibus/engine.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/ibus/install.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/ibus/pending.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/ibus/session.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/ibus/textlimit.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/learn/__init__.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/learn/augment.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/learn/clean.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/learn/importer.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/learn/metrics.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/learn/store.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/learn/teacher.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/learn/train.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/llm.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/llm_api.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/models/__init__.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/numbers.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/platform.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/postcorrect.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/postprocess.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/privacy.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/prompt.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/recorder.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/retention.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/server/__init__.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/server/__main__.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/server/app.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/server/config.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/server/runtime.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/setup/__init__.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/setup/configfile.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/setup/extras.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/setup/hotkey.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/setup/noninteractive.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/setup/speech.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/setup/steps.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/setup/system.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/setup/ui.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/streaming.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/suggest.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/tokens.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/tray/__init__.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/tray/_indicator.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/tray/_pystray.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/tts/base.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/tts/chain.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/tts/player.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/typers/base.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/typers/chain.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/typers/collecting.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/typers/fcitx_bridge.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/typers/ibus.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/typers/manager.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/typers/ydotool.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/ui.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/vad.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/vocab_stats.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/vocabulary.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/watchdog.py +0 -0
- {python_voiceio-1.2.0 → python_voiceio-1.4.0}/voiceio/wordfreq.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-voiceio
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: Voice dictation for Linux. Speak → text, locally, instantly.
|
|
5
5
|
Author: Hugo Montenegro
|
|
6
6
|
License-Expression: MIT
|
|
7
|
-
Project-URL: Homepage, https://
|
|
7
|
+
Project-URL: Homepage, https://voiceio.dev/
|
|
8
8
|
Project-URL: Repository, https://github.com/Hugo0/voiceio
|
|
9
9
|
Project-URL: Issues, https://github.com/Hugo0/voiceio/issues
|
|
10
10
|
Project-URL: Changelog, https://github.com/Hugo0/voiceio/releases
|
|
@@ -58,7 +58,7 @@ Dynamic: license-file
|
|
|
58
58
|
[](https://pypi.org/project/python-voiceio/)
|
|
59
59
|
[](LICENSE)
|
|
60
60
|
|
|
61
|
-
**Voice dictation that learns you.** Local, open source, private — and it fine-tunes itself on your own voice, on your own machine.
|
|
61
|
+
**Voice dictation that learns you.** ([website](https://voiceio.dev/)) Local, open source, private — and it fine-tunes itself on your own voice, on your own machine.
|
|
62
62
|
|
|
63
63
|
Off-the-shelf speech models stumble on exactly the words you say most: your colleagues' names, your project, your jargon. voiceio keeps (with your permission) the audio of what you dictate, has a bigger model transcribe it overnight, and trains a personal model from it — nothing uploaded, no GPU required. On the author's dictation, one overnight run on a laptop CPU cut word errors from **12.1% to 5.0%** on held-out clips and recognized more of the author's vocabulary, at the same speed. Every model is scored on clips it never trained on, and it only replaces the current one when it wins.
|
|
64
64
|
|
|
@@ -182,7 +182,8 @@ Most dictation errors are proper nouns and jargon: `PostgreSQL` heard as `post g
|
|
|
182
182
|
voiceio learn import DIR --source phone # optional: add your own recordings from elsewhere
|
|
183
183
|
voiceio learn label # a stronger teacher model (large-v3-turbo) transcribes your clips
|
|
184
184
|
voiceio learn review # fix the teacher where it matters: your names and terms
|
|
185
|
-
voiceio learn review --test #
|
|
185
|
+
voiceio learn review --test # verify the held-out clips, so eval scores against the truth: it asks only
|
|
186
|
+
# where the teacher and your live decode disagree, quickest clips first
|
|
186
187
|
voiceio learn augment # synthesize extra examples of rare terms with local TTS
|
|
187
188
|
voiceio learn build # freeze a versioned train/dev/test dataset (the test split never trains)
|
|
188
189
|
voiceio learn eval # score the current model on your held-out clips: WER and term recall
|
|
@@ -190,9 +191,16 @@ voiceio learn eval # score the current model on your held-out clips: WER a
|
|
|
190
191
|
voiceio learn train # LoRA fine-tune (needs the `train` extra; decoder-only on CPU, --full on a GPU)
|
|
191
192
|
voiceio learn promote <run> # dictate with the fine-tune; `voiceio learn rollback` undoes it
|
|
192
193
|
voiceio learn maintain # later: label what's new, and once 50+ clips piled up, retrain and compare
|
|
193
|
-
voiceio learn schedule on # …or let it run `maintain`
|
|
194
|
+
voiceio learn schedule on # …or let it run `maintain` on a schedule, and notify you when a model wins
|
|
195
|
+
# --at 02:00 --every week: when (default daily at 03:30)
|
|
196
|
+
# --auto-promote: switch to a winner by itself; `learn rollback` undoes it
|
|
197
|
+
voiceio learn stop # stop a run in progress (it goes again next time)
|
|
194
198
|
```
|
|
195
199
|
|
|
200
|
+
Scheduled runs wait for a quiet machine: they start only when the 1-minute load per CPU is at most `[learn] max_load` (0.25) and you're on mains power, and give up after `wait_hours` (3) until the next run. When one starts you get a notification with **Stop for now** and **Weekly instead**.
|
|
201
|
+
|
|
202
|
+
To keep a second machine (say, the server your phone dictates through) on the same model, set `[learn] on_promote = "scripts/push_model.sh HOST"`: after every switch it copies the model there and flips a `current` link the server points at.
|
|
203
|
+
|
|
196
204
|
Labeling, training and offline eval run in an idle-priority cgroup, so they never slow dictation down. Everything stays on your machine unless you choose `train --remote`.
|
|
197
205
|
|
|
198
206
|
`voiceio learn status` shows where you are, each model's scores, and suggests `promote` only when a fine-tune wins on everything. `promote` itself refuses a model that hasn't, and reports how many clips got better or worse with a confidence interval — a few dozen test clips can move an average by luck.
|
|
@@ -300,8 +308,6 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
|
|
|
300
308
|
|
|
301
309
|
## Roadmap
|
|
302
310
|
|
|
303
|
-
- **A landing page** — voiceio as local, open-source, private dictation that learns from you. A draft is in [`site/index.html`](site/index.html), ready for GitHub Pages.
|
|
304
|
-
- **Hands-off promotion** — `learn schedule` already retrains nightly and tells you when a model wins; next is switching to it automatically, with one-command rollback.
|
|
305
311
|
- **Unbounded vocabulary** — move to a decoder whose biasing has no token cap (see [model choice](CONTRIBUTING.md#model-choice-why-whisper-and-what-would-change-it)).
|
|
306
312
|
- **One model everywhere** — sync your fine-tune to your phone server and laptops, peer to peer.
|
|
307
313
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://pypi.org/project/python-voiceio/)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
-
**Voice dictation that learns you.** Local, open source, private — and it fine-tunes itself on your own voice, on your own machine.
|
|
8
|
+
**Voice dictation that learns you.** ([website](https://voiceio.dev/)) Local, open source, private — and it fine-tunes itself on your own voice, on your own machine.
|
|
9
9
|
|
|
10
10
|
Off-the-shelf speech models stumble on exactly the words you say most: your colleagues' names, your project, your jargon. voiceio keeps (with your permission) the audio of what you dictate, has a bigger model transcribe it overnight, and trains a personal model from it — nothing uploaded, no GPU required. On the author's dictation, one overnight run on a laptop CPU cut word errors from **12.1% to 5.0%** on held-out clips and recognized more of the author's vocabulary, at the same speed. Every model is scored on clips it never trained on, and it only replaces the current one when it wins.
|
|
11
11
|
|
|
@@ -129,7 +129,8 @@ Most dictation errors are proper nouns and jargon: `PostgreSQL` heard as `post g
|
|
|
129
129
|
voiceio learn import DIR --source phone # optional: add your own recordings from elsewhere
|
|
130
130
|
voiceio learn label # a stronger teacher model (large-v3-turbo) transcribes your clips
|
|
131
131
|
voiceio learn review # fix the teacher where it matters: your names and terms
|
|
132
|
-
voiceio learn review --test #
|
|
132
|
+
voiceio learn review --test # verify the held-out clips, so eval scores against the truth: it asks only
|
|
133
|
+
# where the teacher and your live decode disagree, quickest clips first
|
|
133
134
|
voiceio learn augment # synthesize extra examples of rare terms with local TTS
|
|
134
135
|
voiceio learn build # freeze a versioned train/dev/test dataset (the test split never trains)
|
|
135
136
|
voiceio learn eval # score the current model on your held-out clips: WER and term recall
|
|
@@ -137,9 +138,16 @@ voiceio learn eval # score the current model on your held-out clips: WER a
|
|
|
137
138
|
voiceio learn train # LoRA fine-tune (needs the `train` extra; decoder-only on CPU, --full on a GPU)
|
|
138
139
|
voiceio learn promote <run> # dictate with the fine-tune; `voiceio learn rollback` undoes it
|
|
139
140
|
voiceio learn maintain # later: label what's new, and once 50+ clips piled up, retrain and compare
|
|
140
|
-
voiceio learn schedule on # …or let it run `maintain`
|
|
141
|
+
voiceio learn schedule on # …or let it run `maintain` on a schedule, and notify you when a model wins
|
|
142
|
+
# --at 02:00 --every week: when (default daily at 03:30)
|
|
143
|
+
# --auto-promote: switch to a winner by itself; `learn rollback` undoes it
|
|
144
|
+
voiceio learn stop # stop a run in progress (it goes again next time)
|
|
141
145
|
```
|
|
142
146
|
|
|
147
|
+
Scheduled runs wait for a quiet machine: they start only when the 1-minute load per CPU is at most `[learn] max_load` (0.25) and you're on mains power, and give up after `wait_hours` (3) until the next run. When one starts you get a notification with **Stop for now** and **Weekly instead**.
|
|
148
|
+
|
|
149
|
+
To keep a second machine (say, the server your phone dictates through) on the same model, set `[learn] on_promote = "scripts/push_model.sh HOST"`: after every switch it copies the model there and flips a `current` link the server points at.
|
|
150
|
+
|
|
143
151
|
Labeling, training and offline eval run in an idle-priority cgroup, so they never slow dictation down. Everything stays on your machine unless you choose `train --remote`.
|
|
144
152
|
|
|
145
153
|
`voiceio learn status` shows where you are, each model's scores, and suggests `promote` only when a fine-tune wins on everything. `promote` itself refuses a model that hasn't, and reports how many clips got better or worse with a confidence interval — a few dozen test clips can move an average by luck.
|
|
@@ -247,8 +255,6 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
|
|
|
247
255
|
|
|
248
256
|
## Roadmap
|
|
249
257
|
|
|
250
|
-
- **A landing page** — voiceio as local, open-source, private dictation that learns from you. A draft is in [`site/index.html`](site/index.html), ready for GitHub Pages.
|
|
251
|
-
- **Hands-off promotion** — `learn schedule` already retrains nightly and tells you when a model wins; next is switching to it automatically, with one-command rollback.
|
|
252
258
|
- **Unbounded vocabulary** — move to a decoder whose biasing has no token cap (see [model choice](CONTRIBUTING.md#model-choice-why-whisper-and-what-would-change-it)).
|
|
253
259
|
- **One model everywhere** — sync your fine-tune to your phone server and laptops, peer to peer.
|
|
254
260
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-voiceio"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.4.0"
|
|
8
8
|
description = "Voice dictation for Linux. Speak → text, locally, instantly."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -58,7 +58,7 @@ sherpa = ["sherpa-onnx>=1.12"]
|
|
|
58
58
|
dev = ["pytest>=7.0", "pytest-mock", "ruff>=0.15,<0.18"]
|
|
59
59
|
|
|
60
60
|
[project.urls]
|
|
61
|
-
Homepage = "https://
|
|
61
|
+
Homepage = "https://voiceio.dev/"
|
|
62
62
|
Repository = "https://github.com/Hugo0/voiceio"
|
|
63
63
|
Issues = "https://github.com/Hugo0/voiceio/issues"
|
|
64
64
|
Changelog = "https://github.com/Hugo0/voiceio/releases"
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-voiceio
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: Voice dictation for Linux. Speak → text, locally, instantly.
|
|
5
5
|
Author: Hugo Montenegro
|
|
6
6
|
License-Expression: MIT
|
|
7
|
-
Project-URL: Homepage, https://
|
|
7
|
+
Project-URL: Homepage, https://voiceio.dev/
|
|
8
8
|
Project-URL: Repository, https://github.com/Hugo0/voiceio
|
|
9
9
|
Project-URL: Issues, https://github.com/Hugo0/voiceio/issues
|
|
10
10
|
Project-URL: Changelog, https://github.com/Hugo0/voiceio/releases
|
|
@@ -58,7 +58,7 @@ Dynamic: license-file
|
|
|
58
58
|
[](https://pypi.org/project/python-voiceio/)
|
|
59
59
|
[](LICENSE)
|
|
60
60
|
|
|
61
|
-
**Voice dictation that learns you.** Local, open source, private — and it fine-tunes itself on your own voice, on your own machine.
|
|
61
|
+
**Voice dictation that learns you.** ([website](https://voiceio.dev/)) Local, open source, private — and it fine-tunes itself on your own voice, on your own machine.
|
|
62
62
|
|
|
63
63
|
Off-the-shelf speech models stumble on exactly the words you say most: your colleagues' names, your project, your jargon. voiceio keeps (with your permission) the audio of what you dictate, has a bigger model transcribe it overnight, and trains a personal model from it — nothing uploaded, no GPU required. On the author's dictation, one overnight run on a laptop CPU cut word errors from **12.1% to 5.0%** on held-out clips and recognized more of the author's vocabulary, at the same speed. Every model is scored on clips it never trained on, and it only replaces the current one when it wins.
|
|
64
64
|
|
|
@@ -182,7 +182,8 @@ Most dictation errors are proper nouns and jargon: `PostgreSQL` heard as `post g
|
|
|
182
182
|
voiceio learn import DIR --source phone # optional: add your own recordings from elsewhere
|
|
183
183
|
voiceio learn label # a stronger teacher model (large-v3-turbo) transcribes your clips
|
|
184
184
|
voiceio learn review # fix the teacher where it matters: your names and terms
|
|
185
|
-
voiceio learn review --test #
|
|
185
|
+
voiceio learn review --test # verify the held-out clips, so eval scores against the truth: it asks only
|
|
186
|
+
# where the teacher and your live decode disagree, quickest clips first
|
|
186
187
|
voiceio learn augment # synthesize extra examples of rare terms with local TTS
|
|
187
188
|
voiceio learn build # freeze a versioned train/dev/test dataset (the test split never trains)
|
|
188
189
|
voiceio learn eval # score the current model on your held-out clips: WER and term recall
|
|
@@ -190,9 +191,16 @@ voiceio learn eval # score the current model on your held-out clips: WER a
|
|
|
190
191
|
voiceio learn train # LoRA fine-tune (needs the `train` extra; decoder-only on CPU, --full on a GPU)
|
|
191
192
|
voiceio learn promote <run> # dictate with the fine-tune; `voiceio learn rollback` undoes it
|
|
192
193
|
voiceio learn maintain # later: label what's new, and once 50+ clips piled up, retrain and compare
|
|
193
|
-
voiceio learn schedule on # …or let it run `maintain`
|
|
194
|
+
voiceio learn schedule on # …or let it run `maintain` on a schedule, and notify you when a model wins
|
|
195
|
+
# --at 02:00 --every week: when (default daily at 03:30)
|
|
196
|
+
# --auto-promote: switch to a winner by itself; `learn rollback` undoes it
|
|
197
|
+
voiceio learn stop # stop a run in progress (it goes again next time)
|
|
194
198
|
```
|
|
195
199
|
|
|
200
|
+
Scheduled runs wait for a quiet machine: they start only when the 1-minute load per CPU is at most `[learn] max_load` (0.25) and you're on mains power, and give up after `wait_hours` (3) until the next run. When one starts you get a notification with **Stop for now** and **Weekly instead**.
|
|
201
|
+
|
|
202
|
+
To keep a second machine (say, the server your phone dictates through) on the same model, set `[learn] on_promote = "scripts/push_model.sh HOST"`: after every switch it copies the model there and flips a `current` link the server points at.
|
|
203
|
+
|
|
196
204
|
Labeling, training and offline eval run in an idle-priority cgroup, so they never slow dictation down. Everything stays on your machine unless you choose `train --remote`.
|
|
197
205
|
|
|
198
206
|
`voiceio learn status` shows where you are, each model's scores, and suggests `promote` only when a fine-tune wins on everything. `promote` itself refuses a model that hasn't, and reports how many clips got better or worse with a confidence interval — a few dozen test clips can move an average by luck.
|
|
@@ -300,8 +308,6 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
|
|
|
300
308
|
|
|
301
309
|
## Roadmap
|
|
302
310
|
|
|
303
|
-
- **A landing page** — voiceio as local, open-source, private dictation that learns from you. A draft is in [`site/index.html`](site/index.html), ready for GitHub Pages.
|
|
304
|
-
- **Hands-off promotion** — `learn schedule` already retrains nightly and tells you when a model wins; next is switching to it automatically, with one-command rollback.
|
|
305
311
|
- **Unbounded vocabulary** — move to a decoder whose biasing has no token cap (see [model choice](CONTRIBUTING.md#model-choice-why-whisper-and-what-would-change-it)).
|
|
306
312
|
- **One model everywhere** — sync your fine-tune to your phone server and laptops, peer to peer.
|
|
307
313
|
|
|
@@ -37,6 +37,7 @@ tests/test_learn_clean.py
|
|
|
37
37
|
tests/test_learn_import.py
|
|
38
38
|
tests/test_learn_metrics.py
|
|
39
39
|
tests/test_learn_pipeline.py
|
|
40
|
+
tests/test_learn_quiet.py
|
|
40
41
|
tests/test_learn_review.py
|
|
41
42
|
tests/test_learn_targets.py
|
|
42
43
|
tests/test_llm.py
|
|
@@ -144,6 +145,7 @@ voiceio/learn/clean.py
|
|
|
144
145
|
voiceio/learn/evaluate.py
|
|
145
146
|
voiceio/learn/importer.py
|
|
146
147
|
voiceio/learn/metrics.py
|
|
148
|
+
voiceio/learn/quiet.py
|
|
147
149
|
voiceio/learn/review.py
|
|
148
150
|
voiceio/learn/store.py
|
|
149
151
|
voiceio/learn/teacher.py
|
|
@@ -121,5 +121,5 @@ def test_learning_status_and_tips(monkeypatch):
|
|
|
121
121
|
cfg.model.name = str(learn_dir() / "models" / "v0001-small-x" / "ct2")
|
|
122
122
|
st = _learning_status(cfg)
|
|
123
123
|
assert st["Voice model"] == "fine-tuned on you (v0001-small-x)"
|
|
124
|
-
assert st["Recordings"].startswith("on (") and st["
|
|
124
|
+
assert st["Recordings"].startswith("on (") and st["Scheduled learning"] == "off"
|
|
125
125
|
assert any("learn schedule on" in t for t in _accuracy_tips(st))
|
|
@@ -132,14 +132,34 @@ def test_learn_jobs_reexec_into_an_idle_scope(monkeypatch):
|
|
|
132
132
|
|
|
133
133
|
|
|
134
134
|
def test_promotion_requires_a_winning_eval():
|
|
135
|
-
base = {"wer": 0.10, "term_recall": 0.40, "repetitions": 0}
|
|
136
|
-
better = {"wer": 0.101, "term_recall": 0.70, "repetitions": 0} # vocab win
|
|
137
|
-
worse = {"wer": 0.13, "term_recall": 0.70, "repetitions": 0}
|
|
135
|
+
base = {"wer": 0.10, "term_recall": 0.40, "repetitions": 0, "clips": 44}
|
|
136
|
+
better = {"wer": 0.101, "term_recall": 0.70, "repetitions": 0, "clips": 44} # vocab win
|
|
137
|
+
worse = {"wer": 0.13, "term_recall": 0.70, "repetitions": 0, "clips": 44}
|
|
138
138
|
assert evaluate.promotion_verdict(better, base)[0]
|
|
139
139
|
assert not evaluate.promotion_verdict(worse, base)[0]
|
|
140
140
|
assert not evaluate.promotion_verdict({**better, "repetitions": 2}, base)[0]
|
|
141
141
|
|
|
142
142
|
|
|
143
|
+
def test_no_promotion_without_evidence():
|
|
144
|
+
"""A crashed decode scores 0% WER; an empty test split scores nothing."""
|
|
145
|
+
base = {"wer": 0.10, "term_recall": 0.40, "repetitions": 0, "clips": 44}
|
|
146
|
+
perfect = {"wer": 0.0, "term_recall": 1.0, "repetitions": 0}
|
|
147
|
+
assert not evaluate.promotion_verdict({**perfect, "clips": 44, "failures": 3}, base)[0]
|
|
148
|
+
assert not evaluate.promotion_verdict({**perfect, "clips": 40}, base)[0] # other clips
|
|
149
|
+
assert not evaluate.promotion_verdict({**perfect, "clips": 2}, {**base, "clips": 2})[0]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_latest_finds_runs_at_the_configured_beam():
|
|
153
|
+
runs = store.learn_dir() / "runs"
|
|
154
|
+
runs.mkdir(parents=True)
|
|
155
|
+
for i, (dec, wer) in enumerate([("faster-whisper:small", 5), ("faster-whisper:small@beam2+draft", 2)]):
|
|
156
|
+
(runs / f"eval-2026010{i}.json").write_text(json.dumps(
|
|
157
|
+
{"decoder": dec, "dataset": "v0001", "wer": wer, "metrics": metrics.METRICS_VERSION}))
|
|
158
|
+
assert evaluate.latest("small", "v0001")["wer"] == 5
|
|
159
|
+
assert evaluate.latest("small", "v0001", beam=2)["wer"] == 2
|
|
160
|
+
assert evaluate.latest("small", "v0001", beam=1) is None
|
|
161
|
+
|
|
162
|
+
|
|
143
163
|
def test_latest_eval_matches_model_dataset_and_mode():
|
|
144
164
|
runs = store.learn_dir() / "runs"
|
|
145
165
|
runs.mkdir(parents=True)
|
|
@@ -308,3 +328,58 @@ def test_wait_summary_p90_never_exceeds_max():
|
|
|
308
328
|
s = r.wait_summary()
|
|
309
329
|
p90 = float(s.split("p90 ")[1].split("s")[0])
|
|
310
330
|
assert p90 <= 6.83
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def test_maintain_auto_promote_switches_restarts_and_says_how_to_undo(monkeypatch):
|
|
334
|
+
import argparse
|
|
335
|
+
from types import SimpleNamespace
|
|
336
|
+
|
|
337
|
+
from voiceio import feedback, service
|
|
338
|
+
from voiceio.cli import learn as cli
|
|
339
|
+
from voiceio.learn import evaluate
|
|
340
|
+
for i in range(3):
|
|
341
|
+
_clip(f"{i}.wav", 3, "x")
|
|
342
|
+
store.add_label(f"{i}.wav", [Segment(0, 3, "hello there")], "teacher:fake")
|
|
343
|
+
run = store.learn_dir() / "models" / "v0001-small-x"
|
|
344
|
+
calls = []
|
|
345
|
+
monkeypatch.setattr(cli, "cmd_label", lambda a: None)
|
|
346
|
+
monkeypatch.setattr(cli, "_announce_learning", lambda min_new: None)
|
|
347
|
+
monkeypatch.setattr(train, "missing_train_deps", lambda: [])
|
|
348
|
+
monkeypatch.setattr(cli, "_run_eval", lambda *a, **k: SimpleNamespace(
|
|
349
|
+
wer=0.1, term_recall=0.5, repetitions=0, per_clip=[]))
|
|
350
|
+
monkeypatch.setattr(cli, "_train", lambda *a, **k: run)
|
|
351
|
+
monkeypatch.setattr(evaluate, "promotion_verdict", lambda c, b: (True, "table"))
|
|
352
|
+
monkeypatch.setattr(cli, "cmd_promote", lambda a: calls.append(("promote", a.run)))
|
|
353
|
+
monkeypatch.setattr(service, "is_installed", lambda: True)
|
|
354
|
+
monkeypatch.setattr(service, "restart_when_idle", lambda: calls.append(("restart",)) or True)
|
|
355
|
+
monkeypatch.setattr(feedback, "notify", lambda t, b, urgent=False: calls.append(("notify", b)))
|
|
356
|
+
|
|
357
|
+
cli.cmd_maintain(argparse.Namespace(min_new=1, base="small", epochs=1, auto_promote=True))
|
|
358
|
+
assert calls[:2] == [("promote", "v0001-small-x"), ("restart",)]
|
|
359
|
+
assert "voiceio learn rollback" in calls[2][1]
|
|
360
|
+
|
|
361
|
+
# The gate's own re-check refuses: no restart, no "switched" notification,
|
|
362
|
+
# and no crash (the nightly unit must not fail).
|
|
363
|
+
calls.clear()
|
|
364
|
+
|
|
365
|
+
def refuse(a):
|
|
366
|
+
raise SystemExit("evaluate both models first")
|
|
367
|
+
monkeypatch.setattr(cli, "cmd_promote", refuse)
|
|
368
|
+
cli.cmd_maintain(argparse.Namespace(min_new=1, base="small", epochs=1, auto_promote=True))
|
|
369
|
+
assert calls == []
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def test_promote_and_rollback_run_on_promote_with_the_model(monkeypatch, tmp_path):
|
|
373
|
+
import argparse
|
|
374
|
+
|
|
375
|
+
from voiceio import config
|
|
376
|
+
from voiceio.cli import learn as cli
|
|
377
|
+
run = store.learn_dir() / "models" / "v0002-small-x" / "ct2"
|
|
378
|
+
run.mkdir(parents=True)
|
|
379
|
+
(run / "model.bin").write_text("")
|
|
380
|
+
out = tmp_path / "hook.txt"
|
|
381
|
+
config.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
382
|
+
config.CONFIG_PATH.write_text(f'[learn]\non_promote = "echo $VOICEIO_MODEL >> {out}"\n')
|
|
383
|
+
cli.cmd_promote(argparse.Namespace(run="v0002-small-x", force=True))
|
|
384
|
+
cli.cmd_rollback(argparse.Namespace())
|
|
385
|
+
assert out.read_text().split() == [str(run), "small"]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from voiceio.learn import quiet
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _supply(root, name, kind, online):
|
|
7
|
+
d = root / name
|
|
8
|
+
d.mkdir()
|
|
9
|
+
(d / "type").write_text(kind + "\n")
|
|
10
|
+
(d / "online").write_text(online + "\n")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_on_battery_needs_a_mains_supply_that_is_offline(tmp_path):
|
|
14
|
+
assert not quiet.on_battery(tmp_path) # desktop: no supplies listed
|
|
15
|
+
_supply(tmp_path, "BAT0", "Battery", "0")
|
|
16
|
+
_supply(tmp_path, "AC", "Mains", "0")
|
|
17
|
+
assert quiet.on_battery(tmp_path)
|
|
18
|
+
(tmp_path / "AC" / "online").write_text("1\n")
|
|
19
|
+
assert not quiet.on_battery(tmp_path)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_busy_reason_is_load_per_cpu_then_battery(monkeypatch):
|
|
23
|
+
monkeypatch.setattr(quiet.os, "cpu_count", lambda: 8)
|
|
24
|
+
monkeypatch.setattr(quiet, "on_battery", lambda: False)
|
|
25
|
+
monkeypatch.setattr(quiet.os, "getloadavg", lambda: (4.0, 0, 0)) # 0.5 per CPU
|
|
26
|
+
assert "load 0.50 per CPU" in quiet.busy_reason(0.25)
|
|
27
|
+
monkeypatch.setattr(quiet.os, "getloadavg", lambda: (1.0, 0, 0))
|
|
28
|
+
assert quiet.busy_reason(0.25) is None
|
|
29
|
+
monkeypatch.setattr(quiet, "on_battery", lambda: True)
|
|
30
|
+
assert quiet.busy_reason(0.25) == "on battery"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_wait_until_quiet_waits_then_gives_up(monkeypatch):
|
|
34
|
+
now = [0.0]
|
|
35
|
+
sleeps = []
|
|
36
|
+
reasons = iter(["busy", "busy", None])
|
|
37
|
+
monkeypatch.setattr(quiet, "busy_reason", lambda _: next(reasons))
|
|
38
|
+
args = dict(poll=300, sleep=lambda s: (sleeps.append(s), now.__setitem__(0, now[0] + s)),
|
|
39
|
+
clock=lambda: now[0], say=lambda _: None)
|
|
40
|
+
assert quiet.wait_until_quiet(0.25, 3, **args)
|
|
41
|
+
assert sleeps == [300, 300]
|
|
42
|
+
|
|
43
|
+
monkeypatch.setattr(quiet, "busy_reason", lambda _: "on battery")
|
|
44
|
+
said = []
|
|
45
|
+
now[0] = 0.0
|
|
46
|
+
assert not quiet.wait_until_quiet(0.25, 1, **{**args, "say": said.append})
|
|
47
|
+
assert now[0] <= 3600 and said[-1].startswith("still on battery after 1 h")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_scheduled_maintain_waits_for_a_quiet_machine(monkeypatch):
|
|
51
|
+
from voiceio.cli import learn as cli
|
|
52
|
+
monkeypatch.setattr(cli, "_yield_cpu", lambda: None)
|
|
53
|
+
monkeypatch.setattr(quiet, "wait_until_quiet", lambda *a, **k: False)
|
|
54
|
+
ran = []
|
|
55
|
+
monkeypatch.setattr(cli, "cmd_label", lambda a: ran.append("label"))
|
|
56
|
+
cli.cmd_maintain(argparse.Namespace(min_new=5, base="small", epochs=1, when_idle=True))
|
|
57
|
+
assert ran == [] # busy all night: nothing ran
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_learning_announcement_offers_stop_and_weekly(monkeypatch, tmp_path):
|
|
61
|
+
from voiceio import feedback
|
|
62
|
+
from voiceio.cli import learn as cli
|
|
63
|
+
sent = []
|
|
64
|
+
monkeypatch.setattr(feedback, "notify_actions", lambda *a: sent.append(a))
|
|
65
|
+
monkeypatch.setattr(cli.store, "load_labels", lambda: {})
|
|
66
|
+
monkeypatch.setattr(cli.store, "clips", lambda: [argparse.Namespace(audio=f"a{i}.wav") for i in range(3)])
|
|
67
|
+
monkeypatch.setattr(cli.store, "latest_dataset", lambda: None)
|
|
68
|
+
cli._announce_learning(min_new=50)
|
|
69
|
+
(title, body, actions, _), = sent
|
|
70
|
+
assert "learning" in title and body.startswith("Labeling 3 new recordings,")
|
|
71
|
+
assert "on this computer" in body and set(actions) == {"stop", "weekly"}
|
|
72
|
+
|
|
73
|
+
sent.clear()
|
|
74
|
+
monkeypatch.setattr(cli.store, "load_labels", lambda: {f"a{i}.wav": 1 for i in range(3)})
|
|
75
|
+
monkeypatch.setattr(cli.store, "latest_dataset", lambda: tmp_path)
|
|
76
|
+
monkeypatch.setattr(cli.store, "dataset_clips", lambda _: {f"a{i}.wav" for i in range(3)})
|
|
77
|
+
cli._announce_learning(min_new=50)
|
|
78
|
+
assert sent == [] # nothing to do: no notification
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_notify_actions_runs_the_picked_action(monkeypatch):
|
|
82
|
+
import subprocess
|
|
83
|
+
|
|
84
|
+
from voiceio import feedback
|
|
85
|
+
monkeypatch.setattr("sys.platform", "linux")
|
|
86
|
+
monkeypatch.setattr(feedback, "_which", lambda _: "/usr/bin/notify-send")
|
|
87
|
+
monkeypatch.setattr(feedback, "_notify_send_has_actions", lambda: True)
|
|
88
|
+
calls = []
|
|
89
|
+
|
|
90
|
+
def fake_run(args, **kw):
|
|
91
|
+
calls.append(args)
|
|
92
|
+
return subprocess.CompletedProcess(args, 0, stdout="weekly\n")
|
|
93
|
+
monkeypatch.setattr(feedback.subprocess, "run", fake_run)
|
|
94
|
+
picked = []
|
|
95
|
+
|
|
96
|
+
class Now: # run the waiting thread inline
|
|
97
|
+
def __init__(self, target, daemon): self.target = target
|
|
98
|
+
def start(self): self.target()
|
|
99
|
+
monkeypatch.setattr(feedback.threading, "Thread", Now)
|
|
100
|
+
feedback.notify_actions("t", "b", {"stop": "Stop", "weekly": "Weekly"}, picked.append)
|
|
101
|
+
assert picked == ["weekly"]
|
|
102
|
+
assert "--wait" in calls[0] and "--action=stop=Stop" in calls[0]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
def test_queue_all_covers_every_segment_of_unreviewed_clips():
|
|
4
|
+
from voiceio.learn import review
|
|
5
|
+
from voiceio.learn.store import Clip, Label, Segment
|
|
6
|
+
a = Clip("a.wav", 0, 4, "", "")
|
|
7
|
+
b = Clip("b.wav", 0, 4, "", "")
|
|
8
|
+
segs = (Segment(0, 2, "one"), Segment(2, 4, "two"))
|
|
9
|
+
labels = {"a.wav": Label("a.wav", segs, "teacher:x", 0),
|
|
10
|
+
"b.wav": Label("b.wav", segs, "human", 0)}
|
|
11
|
+
items = review.queue_all([a, b], labels)
|
|
12
|
+
assert [(i.clip.audio, i.index) for i in items] == [("a.wav", 0), ("a.wav", 1)]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_live_span_shows_the_live_decode_only_where_it_differs():
|
|
16
|
+
from voiceio.learn.review import live_span
|
|
17
|
+
live = "okay so we use kalchi daily and then more stuff"
|
|
18
|
+
assert live_span("We use Kalshi daily.", live) == "we use kalchi daily"
|
|
19
|
+
assert live_span("We use Kalshi daily.", "so we use Kalshi daily ok") is None
|
|
20
|
+
assert live_span("totally different words here", "nothing alike at all") is None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_gold_review_asks_only_where_the_models_disagree_cheapest_clip_first():
|
|
24
|
+
from voiceio.learn import review
|
|
25
|
+
from voiceio.learn.store import Clip, Label, Segment
|
|
26
|
+
segs = (Segment(0, 2, "we use Kalshi daily"), Segment(2, 4, "ship it today"))
|
|
27
|
+
two = Clip("two.wav", 0, 4, "we use kalchi daily ship it to day", "")
|
|
28
|
+
one = Clip("one.wav", 0, 4, "we use kalchi daily ship it today", "")
|
|
29
|
+
agree = Clip("agree.wav", 0, 4, "we use Kalshi daily ship it today", "")
|
|
30
|
+
labels = {c.audio: Label(c.audio, segs, "teacher:x", 0) for c in (two, one, agree)}
|
|
31
|
+
items = review.queue_all([two, one, agree], labels)
|
|
32
|
+
assert [(i.clip.audio, i.index) for i in items] == [
|
|
33
|
+
("one.wav", 0), # one answer completes a gold clip
|
|
34
|
+
("two.wav", 0), ("two.wav", 1),
|
|
35
|
+
] # agree.wav: nothing to ask
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_gold_review_counts_one_misheard_term_as_disagreement():
|
|
39
|
+
from voiceio.learn import review
|
|
40
|
+
from voiceio.learn.store import Clip, Label, Segment
|
|
41
|
+
segs = (Segment(0, 4, "we ship the Kalshi bot every single day"),)
|
|
42
|
+
clip = Clip("a.wav", 0, 4, "we ship the kalchi bot every single day", "")
|
|
43
|
+
items = review.queue_all([clip], {"a.wav": Label("a.wav", segs, "teacher:x", 0)})
|
|
44
|
+
assert len(items) == 1
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from voiceio import service
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_install_symlinks_relinks_and_drops_legacy(monkeypatch, tmp_path):
|
|
8
|
+
venv_bin = tmp_path / "venv" / "bin"
|
|
9
|
+
venv_bin.mkdir(parents=True)
|
|
10
|
+
for name in service.SCRIPT_NAMES:
|
|
11
|
+
(venv_bin / name).write_text("#!/bin/sh\n")
|
|
12
|
+
old = tmp_path / "old"
|
|
13
|
+
old.mkdir()
|
|
14
|
+
local_bin = tmp_path / "local-bin"
|
|
15
|
+
local_bin.mkdir()
|
|
16
|
+
(local_bin / "voiceio").symlink_to(old / "voiceio") # stale install
|
|
17
|
+
(local_bin / "voiceio-doctor").symlink_to(old / "voiceio-doctor")
|
|
18
|
+
monkeypatch.setattr(service, "LOCAL_BIN", local_bin)
|
|
19
|
+
monkeypatch.setattr(sys, "prefix", str(tmp_path / "venv"))
|
|
20
|
+
monkeypatch.setattr(service, "_is_pipx_install", lambda: False)
|
|
21
|
+
monkeypatch.setattr(service, "_local_bin_on_path", lambda: True)
|
|
22
|
+
|
|
23
|
+
assert service.install_symlinks() == service.SCRIPT_NAMES
|
|
24
|
+
assert Path(local_bin / "voiceio").resolve() == (venv_bin / "voiceio").resolve()
|
|
25
|
+
assert not (local_bin / "voiceio-doctor").is_symlink()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_learn_timer_runs_maintain_at_idle_priority(monkeypatch, tmp_path):
|
|
29
|
+
calls = []
|
|
30
|
+
monkeypatch.setattr(service, "has_systemd", lambda: True)
|
|
31
|
+
monkeypatch.setattr(service, "_systemctl", lambda *a: calls.append(a) or True)
|
|
32
|
+
monkeypatch.setattr(service, "_find_voiceio_bin", lambda: "/opt/v/bin/voiceio")
|
|
33
|
+
monkeypatch.setattr(service, "SERVICE_DIR", tmp_path)
|
|
34
|
+
monkeypatch.setattr(service, "LEARN_SERVICE_PATH", tmp_path / service.LEARN_SERVICE_NAME)
|
|
35
|
+
monkeypatch.setattr(service, "LEARN_TIMER_PATH", tmp_path / service.LEARN_TIMER_NAME)
|
|
36
|
+
|
|
37
|
+
assert service.install_learn_timer()
|
|
38
|
+
unit = service.LEARN_SERVICE_PATH.read_text()
|
|
39
|
+
assert "ExecStart=/opt/v/bin/voiceio learn maintain --when-idle" in unit
|
|
40
|
+
assert "CPUWeight=idle" in unit and "VOICEIO_IDLE_SCOPE=1" in unit
|
|
41
|
+
assert "Persistent=true" in service.LEARN_TIMER_PATH.read_text()
|
|
42
|
+
assert ("enable", "--now", service.LEARN_TIMER_NAME) in calls
|
|
43
|
+
|
|
44
|
+
service.uninstall_learn_timer()
|
|
45
|
+
assert not service.LEARN_TIMER_PATH.exists() and not service.LEARN_SERVICE_PATH.exists()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_daemon_publishes_state_and_restart_waits_for_idle(monkeypatch):
|
|
49
|
+
import threading
|
|
50
|
+
|
|
51
|
+
from voiceio import config, pidlock
|
|
52
|
+
from voiceio.app import VoiceIO, _State
|
|
53
|
+
from voiceio.cli.servicecmd import wait_until_idle
|
|
54
|
+
|
|
55
|
+
vio = VoiceIO.__new__(VoiceIO)
|
|
56
|
+
vio._state = _State.RECORDING # not the lock holder:
|
|
57
|
+
assert not config.STATE_PATH.exists() # says nothing
|
|
58
|
+
vio._publishes_state = True
|
|
59
|
+
vio._state = _State.RECORDING
|
|
60
|
+
assert config.STATE_PATH.read_text() == "recording"
|
|
61
|
+
|
|
62
|
+
monkeypatch.setattr(pidlock, "daemon_alive", lambda path: False)
|
|
63
|
+
assert wait_until_idle(timeout=5) # dead daemon: stale file, no wait
|
|
64
|
+
monkeypatch.setattr(pidlock, "daemon_alive", lambda path: True)
|
|
65
|
+
threading.Timer(0.3, lambda: setattr(vio, "_state", _State.IDLE)).start()
|
|
66
|
+
assert wait_until_idle(timeout=5, poll=0.05) # waited for the dictation
|
|
67
|
+
vio._state = _State.FINALIZING # a hung finalize: short budget
|
|
68
|
+
assert not wait_until_idle(timeout=60, poll=0.05, finalizing_timeout=0.2)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_daemon_alive_probes_the_pid_lock(tmp_path):
|
|
72
|
+
import fcntl
|
|
73
|
+
|
|
74
|
+
from voiceio.pidlock import daemon_alive
|
|
75
|
+
path = tmp_path / "voiceio.pid"
|
|
76
|
+
assert not daemon_alive(path)
|
|
77
|
+
with open(path, "a") as held:
|
|
78
|
+
fcntl.flock(held, fcntl.LOCK_EX)
|
|
79
|
+
assert daemon_alive(path)
|
|
80
|
+
assert not daemon_alive(path)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_learn_timer_auto_promote_is_opt_in(monkeypatch, tmp_path):
|
|
84
|
+
monkeypatch.setattr(service, "has_systemd", lambda: True)
|
|
85
|
+
monkeypatch.setattr(service, "_systemctl", lambda *a: True)
|
|
86
|
+
monkeypatch.setattr(service, "_find_voiceio_bin", lambda: "voiceio")
|
|
87
|
+
monkeypatch.setattr(service, "LEARN_SERVICE_PATH", tmp_path / "s.service")
|
|
88
|
+
monkeypatch.setattr(service, "LEARN_TIMER_PATH", tmp_path / "s.timer")
|
|
89
|
+
service.install_learn_timer()
|
|
90
|
+
assert not service.learn_timer_auto_promotes()
|
|
91
|
+
service.install_learn_timer(auto_promote=True)
|
|
92
|
+
assert "learn maintain --when-idle --auto-promote" in service.LEARN_SERVICE_PATH.read_text()
|
|
93
|
+
assert service.learn_timer_auto_promotes()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_learn_schedule_comes_from_config(monkeypatch, tmp_path):
|
|
97
|
+
import pytest
|
|
98
|
+
|
|
99
|
+
from voiceio import config
|
|
100
|
+
monkeypatch.setattr(service, "has_systemd", lambda: True)
|
|
101
|
+
monkeypatch.setattr(service, "_systemctl", lambda *a: True)
|
|
102
|
+
monkeypatch.setattr(service, "_find_voiceio_bin", lambda: "voiceio")
|
|
103
|
+
monkeypatch.setattr(service, "LEARN_SERVICE_PATH", tmp_path / "s.service")
|
|
104
|
+
monkeypatch.setattr(service, "LEARN_TIMER_PATH", tmp_path / "s.timer")
|
|
105
|
+
service.install_learn_timer()
|
|
106
|
+
assert "OnCalendar=*-*-* 03:30" in service.LEARN_TIMER_PATH.read_text()
|
|
107
|
+
|
|
108
|
+
config.set_value("learn", "at", "2:05")
|
|
109
|
+
config.set_value("learn", "every", "week")
|
|
110
|
+
service.install_learn_timer()
|
|
111
|
+
assert "OnCalendar=Sun *-*-* 02:05" in service.LEARN_TIMER_PATH.read_text()
|
|
112
|
+
assert service.learn_schedule_summary() == "weekly on Sunday at 2:05"
|
|
113
|
+
|
|
114
|
+
for at, every in (("25:00", "day"), ("3pm", "day"), ("03:30", "month")):
|
|
115
|
+
with pytest.raises(ValueError):
|
|
116
|
+
service.learn_calendar(at, every)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.4.0"
|
|
@@ -43,6 +43,10 @@ class VoiceIO:
|
|
|
43
43
|
@_state.setter
|
|
44
44
|
def _state(self, value: _State) -> None:
|
|
45
45
|
self.__state = value
|
|
46
|
+
# Only the instance holding the PID lock speaks for the daemon: a
|
|
47
|
+
# second launch must not overwrite the running one's state.
|
|
48
|
+
if not getattr(self, "_publishes_state", False):
|
|
49
|
+
return
|
|
46
50
|
try: # best-effort: only `service restart` reads it
|
|
47
51
|
tmp = config.STATE_PATH.with_suffix(".tmp")
|
|
48
52
|
tmp.write_text(value.value)
|
|
@@ -660,7 +664,7 @@ class VoiceIO:
|
|
|
660
664
|
self._typers.redetect_at_start()
|
|
661
665
|
|
|
662
666
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
663
|
-
self._pid_fd = open(PID_PATH, "w"
|
|
667
|
+
self._pid_fd = open(PID_PATH, "a") # not "w": the running daemon's PID stays
|
|
664
668
|
try:
|
|
665
669
|
from voiceio.pidlock import lock_pid_file
|
|
666
670
|
lock_pid_file(self._pid_fd)
|
|
@@ -669,8 +673,11 @@ class VoiceIO:
|
|
|
669
673
|
log.error("Another voiceio instance is already running")
|
|
670
674
|
print("voiceio is already running. Stop it first: voiceio service stop")
|
|
671
675
|
return
|
|
676
|
+
self._pid_fd.truncate(0)
|
|
672
677
|
self._pid_fd.write(str(os.getpid()))
|
|
673
678
|
self._pid_fd.flush()
|
|
679
|
+
self._publishes_state = True
|
|
680
|
+
self._state = self._state
|
|
674
681
|
|
|
675
682
|
self._typers.prepare()
|
|
676
683
|
|