python-voiceio 1.3.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.3.0/python_voiceio.egg-info → python_voiceio-1.4.0}/PKG-INFO +11 -6
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/README.md +9 -4
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/pyproject.toml +2 -2
- {python_voiceio-1.3.0 → python_voiceio-1.4.0/python_voiceio.egg-info}/PKG-INFO +11 -6
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/python_voiceio.egg-info/SOURCES.txt +2 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_health.py +1 -1
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_learn_pipeline.py +17 -0
- python_voiceio-1.4.0/tests/test_learn_quiet.py +102 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_service.py +25 -2
- python_voiceio-1.4.0/voiceio/__init__.py +1 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/cli/learn.py +107 -7
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/config.py +19 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/feedback.py +34 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/health.py +2 -3
- python_voiceio-1.4.0/voiceio/learn/quiet.py +54 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/service.py +49 -10
- python_voiceio-1.3.0/voiceio/__init__.py +0 -1
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/LICENSE +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/python_voiceio.egg-info/requires.txt +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/setup.cfg +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_app_wiring.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_audio_quality.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_audio_source.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_backend_probes.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_cli.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_clipboard_read.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_commands.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_concurrency_lockdown.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_config.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_corrections.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_decoders.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_engine.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_evdev_uaccess.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_fallback.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_fcitx_bridge.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_hints.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_history.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_ibus_engine_limits.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_ibus_engine_listener.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_ibus_pending.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_ibus_pids.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_ibus_ping.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_ibus_session.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_ibus_textlimit.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_ibus_typer.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_learn_clean.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_learn_import.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_learn_metrics.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_learn_review.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_learn_targets.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_llm.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_llm_api.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_numbers.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_pipeline.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_platform.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_postcorrect.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_postprocess.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_prebuffer.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_prompt.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_retention.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_robustness.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_security_hardening.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_server.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_setup.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_streaming.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_suggest.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_tokens.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_transcriber.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_tts.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_typer_manager.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_vad.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_vocabulary.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_wordfreq.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/tests/test_worker_decode.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/__main__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/app.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/audio_source.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/backends.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/cli/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/cli/configcmd.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/cli/corrections.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/cli/doctor.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/cli/history.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/cli/models.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/cli/servicecmd.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/cli/uninstall.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/cli/vocab.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/clipboard_read.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/commands.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/consent.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/core/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/core/bias.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/core/decoders/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/core/decoders/base.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/core/decoders/catalog.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/core/decoders/faster_whisper/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/core/decoders/lanes.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/core/decoders/sherpa.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/core/decoders/whispercpp.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/core/engine.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/core/pipeline.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/corrections.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/data/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/demo.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/display.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/hints.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/history.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/hotkeys/socket_backend.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/ibus/engine.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/ibus/install.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/ibus/pending.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/ibus/session.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/ibus/textlimit.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/learn/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/learn/augment.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/learn/clean.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/learn/evaluate.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/learn/importer.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/learn/metrics.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/learn/review.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/learn/store.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/learn/teacher.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/learn/train.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/llm.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/llm_api.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/models/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/numbers.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/pidlock.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/platform.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/postcorrect.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/postprocess.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/privacy.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/prompt.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/recorder.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/retention.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/server/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/server/__main__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/server/app.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/server/config.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/server/runtime.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/setup/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/setup/configfile.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/setup/extras.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/setup/hotkey.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/setup/noninteractive.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/setup/speech.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/setup/steps.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/setup/system.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/setup/ui.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/streaming.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/suggest.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/tokens.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/tray/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/tray/_indicator.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/tray/_pystray.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/tts/base.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/tts/chain.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/tts/player.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/typers/base.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/typers/chain.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/typers/collecting.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/typers/fcitx_bridge.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/typers/ibus.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/typers/manager.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/typers/ydotool.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/ui.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/vad.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/vocab_stats.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/vocabulary.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.4.0}/voiceio/watchdog.py +0 -0
- {python_voiceio-1.3.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
|
|
|
@@ -191,10 +191,16 @@ voiceio learn eval # score the current model on your held-out clips: WER a
|
|
|
191
191
|
voiceio learn train # LoRA fine-tune (needs the `train` extra; decoder-only on CPU, --full on a GPU)
|
|
192
192
|
voiceio learn promote <run> # dictate with the fine-tune; `voiceio learn rollback` undoes it
|
|
193
193
|
voiceio learn maintain # later: label what's new, and once 50+ clips piled up, retrain and compare
|
|
194
|
-
voiceio learn schedule on # …or let it run `maintain`
|
|
195
|
-
#
|
|
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)
|
|
196
198
|
```
|
|
197
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
|
+
|
|
198
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`.
|
|
199
205
|
|
|
200
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.
|
|
@@ -302,7 +308,6 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
|
|
|
302
308
|
|
|
303
309
|
## Roadmap
|
|
304
310
|
|
|
305
|
-
- **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.
|
|
306
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)).
|
|
307
312
|
- **One model everywhere** — sync your fine-tune to your phone server and laptops, peer to peer.
|
|
308
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
|
|
|
@@ -138,10 +138,16 @@ voiceio learn eval # score the current model on your held-out clips: WER a
|
|
|
138
138
|
voiceio learn train # LoRA fine-tune (needs the `train` extra; decoder-only on CPU, --full on a GPU)
|
|
139
139
|
voiceio learn promote <run> # dictate with the fine-tune; `voiceio learn rollback` undoes it
|
|
140
140
|
voiceio learn maintain # later: label what's new, and once 50+ clips piled up, retrain and compare
|
|
141
|
-
voiceio learn schedule on # …or let it run `maintain`
|
|
142
|
-
#
|
|
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)
|
|
143
145
|
```
|
|
144
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
|
+
|
|
145
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`.
|
|
146
152
|
|
|
147
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.
|
|
@@ -249,7 +255,6 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
|
|
|
249
255
|
|
|
250
256
|
## Roadmap
|
|
251
257
|
|
|
252
|
-
- **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.
|
|
253
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)).
|
|
254
259
|
- **One model everywhere** — sync your fine-tune to your phone server and laptops, peer to peer.
|
|
255
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
|
|
|
@@ -191,10 +191,16 @@ voiceio learn eval # score the current model on your held-out clips: WER a
|
|
|
191
191
|
voiceio learn train # LoRA fine-tune (needs the `train` extra; decoder-only on CPU, --full on a GPU)
|
|
192
192
|
voiceio learn promote <run> # dictate with the fine-tune; `voiceio learn rollback` undoes it
|
|
193
193
|
voiceio learn maintain # later: label what's new, and once 50+ clips piled up, retrain and compare
|
|
194
|
-
voiceio learn schedule on # …or let it run `maintain`
|
|
195
|
-
#
|
|
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)
|
|
196
198
|
```
|
|
197
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
|
+
|
|
198
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`.
|
|
199
205
|
|
|
200
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.
|
|
@@ -302,7 +308,6 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
|
|
|
302
308
|
|
|
303
309
|
## Roadmap
|
|
304
310
|
|
|
305
|
-
- **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.
|
|
306
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)).
|
|
307
312
|
- **One model everywhere** — sync your fine-tune to your phone server and laptops, peer to peer.
|
|
308
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))
|
|
@@ -343,6 +343,7 @@ def test_maintain_auto_promote_switches_restarts_and_says_how_to_undo(monkeypatc
|
|
|
343
343
|
run = store.learn_dir() / "models" / "v0001-small-x"
|
|
344
344
|
calls = []
|
|
345
345
|
monkeypatch.setattr(cli, "cmd_label", lambda a: None)
|
|
346
|
+
monkeypatch.setattr(cli, "_announce_learning", lambda min_new: None)
|
|
346
347
|
monkeypatch.setattr(train, "missing_train_deps", lambda: [])
|
|
347
348
|
monkeypatch.setattr(cli, "_run_eval", lambda *a, **k: SimpleNamespace(
|
|
348
349
|
wer=0.1, term_recall=0.5, repetitions=0, per_clip=[]))
|
|
@@ -366,3 +367,19 @@ def test_maintain_auto_promote_switches_restarts_and_says_how_to_undo(monkeypatc
|
|
|
366
367
|
monkeypatch.setattr(cli, "cmd_promote", refuse)
|
|
367
368
|
cli.cmd_maintain(argparse.Namespace(min_new=1, base="small", epochs=1, auto_promote=True))
|
|
368
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]
|
|
@@ -36,7 +36,7 @@ def test_learn_timer_runs_maintain_at_idle_priority(monkeypatch, tmp_path):
|
|
|
36
36
|
|
|
37
37
|
assert service.install_learn_timer()
|
|
38
38
|
unit = service.LEARN_SERVICE_PATH.read_text()
|
|
39
|
-
assert "ExecStart=/opt/v/bin/voiceio learn maintain" in unit
|
|
39
|
+
assert "ExecStart=/opt/v/bin/voiceio learn maintain --when-idle" in unit
|
|
40
40
|
assert "CPUWeight=idle" in unit and "VOICEIO_IDLE_SCOPE=1" in unit
|
|
41
41
|
assert "Persistent=true" in service.LEARN_TIMER_PATH.read_text()
|
|
42
42
|
assert ("enable", "--now", service.LEARN_TIMER_NAME) in calls
|
|
@@ -89,5 +89,28 @@ def test_learn_timer_auto_promote_is_opt_in(monkeypatch, tmp_path):
|
|
|
89
89
|
service.install_learn_timer()
|
|
90
90
|
assert not service.learn_timer_auto_promotes()
|
|
91
91
|
service.install_learn_timer(auto_promote=True)
|
|
92
|
-
assert "learn maintain --auto-promote" in service.LEARN_SERVICE_PATH.read_text()
|
|
92
|
+
assert "learn maintain --when-idle --auto-promote" in service.LEARN_SERVICE_PATH.read_text()
|
|
93
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"
|
|
@@ -86,14 +86,23 @@ def register(sub: argparse._SubParsersAction) -> None:
|
|
|
86
86
|
mt.add_argument("--epochs", type=int, default=4)
|
|
87
87
|
mt.add_argument("--auto-promote", action="store_true",
|
|
88
88
|
help="switch to the fine-tune when it wins, and restart the daemon")
|
|
89
|
+
mt.add_argument("--when-idle", action="store_true",
|
|
90
|
+
help="first wait for a quiet machine on mains power ([learn] max_load, "
|
|
91
|
+
"wait_hours); the scheduled run uses this")
|
|
89
92
|
mt.set_defaults(func=cmd_maintain)
|
|
90
93
|
|
|
91
|
-
sc = lsub.add_parser("schedule", help="Run `maintain`
|
|
94
|
+
sc = lsub.add_parser("schedule", help="Run `maintain` on a schedule (systemd timer)")
|
|
92
95
|
sc.add_argument("state", nargs="?", choices=("on", "off"), help="omit to show")
|
|
96
|
+
sc.add_argument("--at", help="time of day, e.g. 02:00 (saved as [learn] at; default 03:30)")
|
|
97
|
+
sc.add_argument("--every", choices=("day", "week"),
|
|
98
|
+
help="daily, or weekly on Sunday (saved as [learn] every)")
|
|
93
99
|
sc.add_argument("--auto-promote", action="store_true",
|
|
94
100
|
help="with on: switch to a fine-tune that wins, without asking")
|
|
95
101
|
sc.set_defaults(func=cmd_schedule)
|
|
96
102
|
|
|
103
|
+
st = lsub.add_parser("stop", help="Stop a scheduled learning run that is in progress")
|
|
104
|
+
st.set_defaults(func=cmd_stop)
|
|
105
|
+
|
|
97
106
|
pr = lsub.add_parser("promote", help="Use a trained model for dictation")
|
|
98
107
|
pr.add_argument("run", help="run name under learn/models (see `learn status`)")
|
|
99
108
|
pr.add_argument("--force", action="store_true",
|
|
@@ -433,7 +442,12 @@ def cmd_maintain(args: argparse.Namespace) -> None:
|
|
|
433
442
|
"""Label what's new; once enough has piled up, rebuild, retrain and say
|
|
434
443
|
whether the result beats the current model. Never promotes by itself."""
|
|
435
444
|
_yield_cpu()
|
|
436
|
-
from voiceio.learn import evaluate
|
|
445
|
+
from voiceio.learn import evaluate, quiet
|
|
446
|
+
learn_cfg = _cfg().learn
|
|
447
|
+
if getattr(args, "when_idle", False) and not quiet.wait_until_quiet(
|
|
448
|
+
learn_cfg.max_load, learn_cfg.wait_hours):
|
|
449
|
+
return
|
|
450
|
+
_announce_learning(args.min_new)
|
|
437
451
|
cmd_label(argparse.Namespace(teacher=None, limit=None, relabel=False))
|
|
438
452
|
from voiceio.learn import train
|
|
439
453
|
if missing := train.missing_train_deps():
|
|
@@ -484,15 +498,75 @@ def cmd_maintain(args: argparse.Namespace) -> None:
|
|
|
484
498
|
urgent=True)
|
|
485
499
|
|
|
486
500
|
|
|
501
|
+
def _announce_learning(min_new: int) -> None:
|
|
502
|
+
"""Tell the user a run is starting (it takes a while and uses the CPU),
|
|
503
|
+
with buttons to stop it or to learn weekly instead. Silent when there is
|
|
504
|
+
nothing to do."""
|
|
505
|
+
labels = store.load_labels()
|
|
506
|
+
clips = [c.audio for c in store.clips()]
|
|
507
|
+
to_label = sum(1 for a in clips if a not in labels)
|
|
508
|
+
ds = store.latest_dataset()
|
|
509
|
+
new = len(set(clips) - (store.dataset_clips(ds) if ds else set()))
|
|
510
|
+
retrain = new >= min_new
|
|
511
|
+
if not to_label and not retrain:
|
|
512
|
+
return
|
|
513
|
+
from voiceio import feedback
|
|
514
|
+
what = (f"Labeling {to_label} new recordings" if to_label else "") + \
|
|
515
|
+
(", then retraining" if to_label and retrain else "Retraining" if retrain else "")
|
|
516
|
+
feedback.notify_actions(
|
|
517
|
+
"VoiceIO is learning your voice",
|
|
518
|
+
f"{what}, on this computer at idle priority; dictation stays fast.",
|
|
519
|
+
{"stop": "Stop for now", "weekly": "Weekly instead"},
|
|
520
|
+
_on_learning_action)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _on_learning_action(key: str) -> None:
|
|
524
|
+
import signal
|
|
525
|
+
|
|
526
|
+
from voiceio import config, feedback, service
|
|
527
|
+
if key == "weekly":
|
|
528
|
+
config.set_value("learn", "every", "week")
|
|
529
|
+
service.install_learn_timer(auto_promote=service.learn_timer_auto_promotes())
|
|
530
|
+
feedback.notify("VoiceIO: learning weekly now",
|
|
531
|
+
f"{service.learn_schedule_summary()}. This run continues; "
|
|
532
|
+
"change it with voiceio learn schedule on --every day")
|
|
533
|
+
elif key == "stop":
|
|
534
|
+
# Under the timer, stop the unit so training subprocesses go too.
|
|
535
|
+
if not (os.environ.get("INVOCATION_ID") and service.stop_learning()):
|
|
536
|
+
os.kill(os.getpid(), signal.SIGINT)
|
|
537
|
+
|
|
538
|
+
|
|
487
539
|
def _schedule_state() -> str:
|
|
488
540
|
from voiceio import service
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
541
|
+
summary = service.learn_schedule_summary()
|
|
542
|
+
return "off (voiceio learn schedule on)" if summary == "off" else f"on, {summary}"
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def cmd_stop(args: argparse.Namespace) -> None:
|
|
546
|
+
from voiceio import service
|
|
547
|
+
if not service.learning_now():
|
|
548
|
+
print("no learning run in progress")
|
|
549
|
+
elif service.stop_learning():
|
|
550
|
+
print("stopped; it runs again at the next scheduled time")
|
|
551
|
+
else:
|
|
552
|
+
raise SystemExit("could not stop voiceio-learn.service")
|
|
492
553
|
|
|
493
554
|
|
|
494
555
|
def cmd_schedule(args: argparse.Namespace) -> None:
|
|
495
556
|
from voiceio import config, service
|
|
557
|
+
if args.at or args.every:
|
|
558
|
+
learn = config.load().learn
|
|
559
|
+
try: # validate before saving: the timer is generated from these
|
|
560
|
+
service.learn_calendar(args.at or learn.at, args.every or learn.every)
|
|
561
|
+
except ValueError as e:
|
|
562
|
+
raise SystemExit(str(e)) from None
|
|
563
|
+
for key in ("at", "every"):
|
|
564
|
+
if getattr(args, key):
|
|
565
|
+
config.set_value("learn", key, getattr(args, key))
|
|
566
|
+
if args.state is None and service.learn_timer_enabled(): # reschedule in place
|
|
567
|
+
service.install_learn_timer(auto_promote=service.learn_timer_auto_promotes())
|
|
568
|
+
print(f"on, {service.learn_schedule_summary()}")
|
|
569
|
+
return
|
|
496
570
|
if args.state == "on":
|
|
497
571
|
if not config.load().data.retain_audio:
|
|
498
572
|
print("note: [data] retain_audio is off, so there is nothing new to learn from;\n"
|
|
@@ -503,8 +577,11 @@ def cmd_schedule(args: argparse.Namespace) -> None:
|
|
|
503
577
|
f" to also retrain: {train.TRAIN_HINT}")
|
|
504
578
|
if not service.install_learn_timer(auto_promote=args.auto_promote):
|
|
505
579
|
raise SystemExit("could not enable the timer (needs systemd --user)")
|
|
506
|
-
|
|
507
|
-
|
|
580
|
+
learn = config.load().learn
|
|
581
|
+
print(f"on, {service.learn_schedule_summary()}: once the machine is quiet (load per CPU "
|
|
582
|
+
f"≤ {learn.max_load:g}, on mains power; waits up to {learn.wait_hours:g} h), "
|
|
583
|
+
"`voiceio learn maintain` labels new dictation at idle priority, retrains once "
|
|
584
|
+
"50+ clips are new, and "
|
|
508
585
|
+ ("switches to the result when it wins on your held-out clips "
|
|
509
586
|
"(undo: voiceio learn rollback)." if args.auto_promote else
|
|
510
587
|
"tells you if the result is better. It never switches models by itself."))
|
|
@@ -532,6 +609,28 @@ def cmd_promote(args: argparse.Namespace) -> None:
|
|
|
532
609
|
f.write(json.dumps({"ts": time.time(), "from": _cfg().model.name, "to": str(ct2)}) + "\n")
|
|
533
610
|
set_value("model", "name", str(ct2))
|
|
534
611
|
print(f"now dictating with {args.run} — restart the daemon: voiceio service restart")
|
|
612
|
+
_on_promote(str(ct2))
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def _on_promote(model: str) -> None:
|
|
616
|
+
"""Run `[learn] on_promote` for a model switch. Its failure is reported,
|
|
617
|
+
never fatal: the switch on this machine already happened."""
|
|
618
|
+
import os
|
|
619
|
+
import subprocess
|
|
620
|
+
cmd = _cfg().learn.on_promote
|
|
621
|
+
if not cmd:
|
|
622
|
+
return
|
|
623
|
+
print(f"on_promote: {cmd}", flush=True)
|
|
624
|
+
try:
|
|
625
|
+
rc = subprocess.run(cmd, shell=True, env={**os.environ, "VOICEIO_MODEL": model},
|
|
626
|
+
timeout=3600).returncode
|
|
627
|
+
except subprocess.TimeoutExpired:
|
|
628
|
+
rc = "timeout"
|
|
629
|
+
if rc != 0:
|
|
630
|
+
print(f"on_promote failed ({rc})")
|
|
631
|
+
from voiceio import feedback
|
|
632
|
+
feedback.notify("VoiceIO: model switched here, but on_promote failed",
|
|
633
|
+
f"`{cmd}` exited {rc}; run it again by hand.")
|
|
535
634
|
|
|
536
635
|
|
|
537
636
|
def _check_beats_current(ct2: Path) -> None:
|
|
@@ -563,3 +662,4 @@ def cmd_rollback(args: argparse.Namespace) -> None:
|
|
|
563
662
|
set_value("model", "name", last["from"])
|
|
564
663
|
history.write_text("".join(r + "\n" for r in rows[:-1]))
|
|
565
664
|
print(f"back to {last['from']} — restart the daemon: voiceio service restart")
|
|
665
|
+
_on_promote(last["from"])
|
|
@@ -243,6 +243,23 @@ class DataConfig:
|
|
|
243
243
|
capture_intermediates: bool = False
|
|
244
244
|
|
|
245
245
|
|
|
246
|
+
@dataclass
|
|
247
|
+
class LearnConfig:
|
|
248
|
+
# A command run after every model switch (promote, auto-promote,
|
|
249
|
+
# rollback), with the model in $VOICEIO_MODEL — e.g. to push a fine-tune
|
|
250
|
+
# to another machine: scripts/push_model.sh HOST. Empty: nothing runs.
|
|
251
|
+
on_promote: str = ""
|
|
252
|
+
# When nightly learning runs (`voiceio learn schedule on`): a time of day,
|
|
253
|
+
# and "day" or "week" (weekly runs on Sunday).
|
|
254
|
+
at: str = "03:30"
|
|
255
|
+
every: str = "day"
|
|
256
|
+
# It only starts on a quiet machine on mains power: 1-minute load per CPU
|
|
257
|
+
# at most max_load. It waits up to wait_hours for that, then tries again
|
|
258
|
+
# at the next scheduled run.
|
|
259
|
+
max_load: float = 0.25
|
|
260
|
+
wait_hours: float = 3.0
|
|
261
|
+
|
|
262
|
+
|
|
246
263
|
@dataclass
|
|
247
264
|
class Config:
|
|
248
265
|
hotkey: HotkeyConfig = field(default_factory=HotkeyConfig)
|
|
@@ -260,6 +277,7 @@ class Config:
|
|
|
260
277
|
health: HealthConfig = field(default_factory=HealthConfig)
|
|
261
278
|
data: DataConfig = field(default_factory=DataConfig)
|
|
262
279
|
history: HistoryConfig = field(default_factory=HistoryConfig)
|
|
280
|
+
learn: LearnConfig = field(default_factory=LearnConfig)
|
|
263
281
|
|
|
264
282
|
|
|
265
283
|
_REMOVED_KEYS = {
|
|
@@ -340,6 +358,7 @@ def load(path: Path | None = None) -> Config:
|
|
|
340
358
|
health=_build(HealthConfig, raw.get("health", {})),
|
|
341
359
|
data=_build(DataConfig, raw.get("data", {})),
|
|
342
360
|
history=_build(HistoryConfig, raw.get("history", {})),
|
|
361
|
+
learn=_build(LearnConfig, raw.get("learn", {})),
|
|
343
362
|
)
|
|
344
363
|
|
|
345
364
|
|
|
@@ -14,6 +14,7 @@ import shutil
|
|
|
14
14
|
import subprocess
|
|
15
15
|
import threading
|
|
16
16
|
import wave
|
|
17
|
+
from collections.abc import Callable
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
|
|
19
20
|
import numpy as np
|
|
@@ -151,6 +152,39 @@ def _send_notification_linux(title: str, body: str, urgent: bool = False) -> Non
|
|
|
151
152
|
pass
|
|
152
153
|
|
|
153
154
|
|
|
155
|
+
def notify_actions(title: str, body: str, actions: dict[str, str],
|
|
156
|
+
on_action: Callable[[str], None]) -> None:
|
|
157
|
+
"""A notification with buttons ({key: label}); ``on_action(key)`` runs
|
|
158
|
+
when one is clicked. Linux only; elsewhere, or with a notify-send too old
|
|
159
|
+
for actions, it degrades to a plain notification. Never blocks."""
|
|
160
|
+
import sys
|
|
161
|
+
if sys.platform != "linux" or not _which("notify-send") or not _notify_send_has_actions():
|
|
162
|
+
notify(title, body)
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
def wait() -> None:
|
|
166
|
+
args = ["notify-send", "--app-name=VoiceIO", "--wait",
|
|
167
|
+
*(f"--action={k}={label}" for k, label in actions.items()), title, body]
|
|
168
|
+
try:
|
|
169
|
+
picked = subprocess.run(args, capture_output=True, text=True).stdout.strip()
|
|
170
|
+
except OSError:
|
|
171
|
+
return
|
|
172
|
+
if picked in actions:
|
|
173
|
+
on_action(picked)
|
|
174
|
+
|
|
175
|
+
threading.Thread(target=wait, daemon=True).start()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@functools.lru_cache(maxsize=1)
|
|
179
|
+
def _notify_send_has_actions() -> bool:
|
|
180
|
+
"""--action/--wait arrived in libnotify 0.7.9."""
|
|
181
|
+
try:
|
|
182
|
+
return "--action" in subprocess.run(["notify-send", "--help"], capture_output=True,
|
|
183
|
+
text=True, timeout=3).stdout
|
|
184
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
|
|
154
188
|
def _send_notification_windows(title: str, body: str) -> None:
|
|
155
189
|
try:
|
|
156
190
|
from win11toast import notify
|
|
@@ -247,8 +247,7 @@ def _learning_status(cfg) -> dict[str, str]:
|
|
|
247
247
|
out["Recordings"] = f"on ({n} kept to learn from)"
|
|
248
248
|
else:
|
|
249
249
|
out["Recordings"] = "off (nothing to learn from)"
|
|
250
|
-
out["
|
|
251
|
-
"on (auto-promote)" if service.learn_timer_auto_promotes() else "on")
|
|
250
|
+
out["Scheduled learning"] = service.learn_schedule_summary()
|
|
252
251
|
return out
|
|
253
252
|
|
|
254
253
|
|
|
@@ -354,7 +353,7 @@ def _accuracy_tips(accuracy: dict[str, str]) -> list[str]:
|
|
|
354
353
|
tips.append("Add names/jargon to ~/.config/voiceio/vocabulary.txt")
|
|
355
354
|
if accuracy.get("Recordings", "").startswith("off"):
|
|
356
355
|
tips.append("voiceio config set data.retain_audio true — keep recordings so voiceio can learn your voice")
|
|
357
|
-
elif accuracy.get("
|
|
356
|
+
elif accuracy.get("Scheduled learning") == "off":
|
|
358
357
|
tips.append("voiceio learn schedule on — retrain on your voice every night")
|
|
359
358
|
return tips
|
|
360
359
|
|