python-voiceio 1.3.0__tar.gz → 1.5.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.5.0}/PKG-INFO +19 -7
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/README.md +17 -5
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/pyproject.toml +2 -2
- {python_voiceio-1.3.0 → python_voiceio-1.5.0/python_voiceio.egg-info}/PKG-INFO +19 -7
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/python_voiceio.egg-info/SOURCES.txt +2 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_health.py +1 -1
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_learn_pipeline.py +119 -0
- python_voiceio-1.5.0/tests/test_learn_quiet.py +102 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_service.py +25 -2
- python_voiceio-1.5.0/voiceio/__init__.py +1 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/cli/learn.py +136 -16
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/config.py +27 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/feedback.py +34 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/health.py +2 -3
- python_voiceio-1.5.0/voiceio/learn/quiet.py +54 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/learn/train.py +11 -1
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/service.py +49 -10
- python_voiceio-1.3.0/voiceio/__init__.py +0 -1
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/LICENSE +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/python_voiceio.egg-info/requires.txt +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/setup.cfg +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_app_wiring.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_audio_quality.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_audio_source.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_backend_probes.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_cli.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_clipboard_read.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_commands.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_concurrency_lockdown.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_config.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_corrections.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_decoders.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_engine.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_evdev_uaccess.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_fallback.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_fcitx_bridge.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_hints.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_history.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_ibus_engine_limits.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_ibus_engine_listener.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_ibus_pending.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_ibus_pids.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_ibus_ping.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_ibus_session.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_ibus_textlimit.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_ibus_typer.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_learn_clean.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_learn_import.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_learn_metrics.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_learn_review.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_learn_targets.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_llm.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_llm_api.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_numbers.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_pipeline.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_platform.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_postcorrect.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_postprocess.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_prebuffer.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_prompt.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_retention.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_robustness.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_security_hardening.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_server.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_setup.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_streaming.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_suggest.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_tokens.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_transcriber.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_tts.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_typer_manager.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_vad.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_vocabulary.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_wordfreq.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_worker_decode.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/__main__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/app.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/audio_source.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/backends.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/cli/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/cli/configcmd.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/cli/corrections.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/cli/doctor.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/cli/history.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/cli/models.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/cli/servicecmd.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/cli/uninstall.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/cli/vocab.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/clipboard_read.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/commands.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/consent.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/core/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/core/bias.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/core/decoders/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/core/decoders/base.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/core/decoders/catalog.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/core/decoders/faster_whisper/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/core/decoders/lanes.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/core/decoders/sherpa.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/core/decoders/whispercpp.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/core/engine.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/core/pipeline.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/corrections.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/data/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/demo.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/display.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/hints.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/history.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/hotkeys/socket_backend.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/ibus/engine.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/ibus/install.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/ibus/pending.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/ibus/session.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/ibus/textlimit.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/learn/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/learn/augment.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/learn/clean.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/learn/evaluate.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/learn/importer.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/learn/metrics.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/learn/review.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/learn/store.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/learn/teacher.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/llm.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/llm_api.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/models/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/numbers.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/pidlock.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/platform.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/postcorrect.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/postprocess.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/privacy.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/prompt.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/recorder.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/retention.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/server/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/server/__main__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/server/app.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/server/config.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/server/runtime.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/setup/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/setup/configfile.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/setup/extras.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/setup/hotkey.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/setup/noninteractive.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/setup/speech.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/setup/steps.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/setup/system.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/setup/ui.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/streaming.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/suggest.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/tokens.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/tray/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/tray/_indicator.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/tray/_pystray.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/tts/base.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/tts/chain.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/tts/player.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/typers/base.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/typers/chain.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/typers/collecting.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/typers/fcitx_bridge.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/typers/ibus.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/typers/manager.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/typers/ydotool.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/ui.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/vad.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/vocab_stats.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/vocabulary.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/watchdog.py +0 -0
- {python_voiceio-1.3.0 → python_voiceio-1.5.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.5.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
|
|
|
@@ -190,11 +190,19 @@ voiceio learn eval # score the current model on your held-out clips: WER a
|
|
|
190
190
|
# --streaming replays them through live dictation and times stop→text
|
|
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
|
-
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
|
-
#
|
|
193
|
+
voiceio learn maintain # later: label what's new, and once 50+ clips piled up ([learn] min_new), retrain and compare
|
|
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
|
+
What a scheduled run trains comes from `[learn]` too: `base` (default `small`; any Whisper size, `.en` included, or a Hugging Face checkpoint), `teacher` (default `large-v3-turbo`), `epochs` (4) and `min_new` (50); `maintain`'s flags override them. To fine-tune the English-only medium model: `voiceio config set learn.base medium.en` (on CPU, medium trains and dictates roughly three times slower than small).
|
|
203
|
+
|
|
204
|
+
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.
|
|
205
|
+
|
|
198
206
|
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
207
|
|
|
200
208
|
`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 +310,6 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
|
|
|
302
310
|
|
|
303
311
|
## Roadmap
|
|
304
312
|
|
|
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
313
|
- **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
314
|
- **One model everywhere** — sync your fine-tune to your phone server and laptops, peer to peer.
|
|
308
315
|
|
|
@@ -310,6 +317,11 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
|
|
|
310
317
|
|
|
311
318
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for the architecture, conventions and the reasoning behind the model choice. Please open an issue before a large PR.
|
|
312
319
|
|
|
320
|
+
## Credits
|
|
321
|
+
|
|
322
|
+
- Speech recognition: [faster-whisper](https://github.com/SYSTRAN/faster-whisper) and [CTranslate2](https://github.com/OpenNMT/CTranslate2), with OpenAI's [Whisper](https://github.com/openai/whisper) models. The in-browser demo on [voiceio.dev](https://voiceio.dev) runs [Moonshine](https://github.com/usefulsensors/moonshine) through [transformers.js](https://github.com/huggingface/transformers.js).
|
|
323
|
+
- The landing page's look (dithered art, typewriter heading, hairline bento) takes its inspiration from Romario Kavin's [KOLlateral](https://github.com/RomarioKavin1/kollateral).
|
|
324
|
+
|
|
313
325
|
## License
|
|
314
326
|
|
|
315
327
|
MIT. Model weights are not part of voiceio and carry their own licenses (see [Choose a model](#choose-a-model)).
|
|
@@ -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
|
|
|
@@ -137,11 +137,19 @@ voiceio learn eval # score the current model on your held-out clips: WER a
|
|
|
137
137
|
# --streaming replays them through live dictation and times stop→text
|
|
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
|
-
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
|
-
#
|
|
140
|
+
voiceio learn maintain # later: label what's new, and once 50+ clips piled up ([learn] min_new), retrain and compare
|
|
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
|
+
What a scheduled run trains comes from `[learn]` too: `base` (default `small`; any Whisper size, `.en` included, or a Hugging Face checkpoint), `teacher` (default `large-v3-turbo`), `epochs` (4) and `min_new` (50); `maintain`'s flags override them. To fine-tune the English-only medium model: `voiceio config set learn.base medium.en` (on CPU, medium trains and dictates roughly three times slower than small).
|
|
150
|
+
|
|
151
|
+
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.
|
|
152
|
+
|
|
145
153
|
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
154
|
|
|
147
155
|
`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 +257,6 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
|
|
|
249
257
|
|
|
250
258
|
## Roadmap
|
|
251
259
|
|
|
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
260
|
- **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
261
|
- **One model everywhere** — sync your fine-tune to your phone server and laptops, peer to peer.
|
|
255
262
|
|
|
@@ -257,6 +264,11 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
|
|
|
257
264
|
|
|
258
265
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for the architecture, conventions and the reasoning behind the model choice. Please open an issue before a large PR.
|
|
259
266
|
|
|
267
|
+
## Credits
|
|
268
|
+
|
|
269
|
+
- Speech recognition: [faster-whisper](https://github.com/SYSTRAN/faster-whisper) and [CTranslate2](https://github.com/OpenNMT/CTranslate2), with OpenAI's [Whisper](https://github.com/openai/whisper) models. The in-browser demo on [voiceio.dev](https://voiceio.dev) runs [Moonshine](https://github.com/usefulsensors/moonshine) through [transformers.js](https://github.com/huggingface/transformers.js).
|
|
270
|
+
- The landing page's look (dithered art, typewriter heading, hairline bento) takes its inspiration from Romario Kavin's [KOLlateral](https://github.com/RomarioKavin1/kollateral).
|
|
271
|
+
|
|
260
272
|
## License
|
|
261
273
|
|
|
262
274
|
MIT. Model weights are not part of voiceio and carry their own licenses (see [Choose a model](#choose-a-model)).
|
|
@@ -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.5.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.5.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
|
|
|
@@ -190,11 +190,19 @@ voiceio learn eval # score the current model on your held-out clips: WER a
|
|
|
190
190
|
# --streaming replays them through live dictation and times stop→text
|
|
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
|
-
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
|
-
#
|
|
193
|
+
voiceio learn maintain # later: label what's new, and once 50+ clips piled up ([learn] min_new), retrain and compare
|
|
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
|
+
What a scheduled run trains comes from `[learn]` too: `base` (default `small`; any Whisper size, `.en` included, or a Hugging Face checkpoint), `teacher` (default `large-v3-turbo`), `epochs` (4) and `min_new` (50); `maintain`'s flags override them. To fine-tune the English-only medium model: `voiceio config set learn.base medium.en` (on CPU, medium trains and dictates roughly three times slower than small).
|
|
203
|
+
|
|
204
|
+
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.
|
|
205
|
+
|
|
198
206
|
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
207
|
|
|
200
208
|
`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 +310,6 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
|
|
|
302
310
|
|
|
303
311
|
## Roadmap
|
|
304
312
|
|
|
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
313
|
- **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
314
|
- **One model everywhere** — sync your fine-tune to your phone server and laptops, peer to peer.
|
|
308
315
|
|
|
@@ -310,6 +317,11 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
|
|
|
310
317
|
|
|
311
318
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for the architecture, conventions and the reasoning behind the model choice. Please open an issue before a large PR.
|
|
312
319
|
|
|
320
|
+
## Credits
|
|
321
|
+
|
|
322
|
+
- Speech recognition: [faster-whisper](https://github.com/SYSTRAN/faster-whisper) and [CTranslate2](https://github.com/OpenNMT/CTranslate2), with OpenAI's [Whisper](https://github.com/openai/whisper) models. The in-browser demo on [voiceio.dev](https://voiceio.dev) runs [Moonshine](https://github.com/usefulsensors/moonshine) through [transformers.js](https://github.com/huggingface/transformers.js).
|
|
323
|
+
- The landing page's look (dithered art, typewriter heading, hairline bento) takes its inspiration from Romario Kavin's [KOLlateral](https://github.com/RomarioKavin1/kollateral).
|
|
324
|
+
|
|
313
325
|
## License
|
|
314
326
|
|
|
315
327
|
MIT. Model weights are not part of voiceio and carry their own licenses (see [Choose a model](#choose-a-model)).
|
|
@@ -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,121 @@ 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"]
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _maintain_calls(monkeypatch, min_new_clips: int = 3) -> dict:
|
|
389
|
+
"""Run-ready maintain with labeling/training/eval faked; records what they got."""
|
|
390
|
+
from types import SimpleNamespace
|
|
391
|
+
|
|
392
|
+
from voiceio.cli import learn as cli
|
|
393
|
+
for i in range(min_new_clips):
|
|
394
|
+
_clip(f"{i}.wav", 3, "x")
|
|
395
|
+
store.add_label(f"{i}.wav", [Segment(0, 3, "hello there")], "teacher:fake")
|
|
396
|
+
got: dict = {}
|
|
397
|
+
monkeypatch.setattr(cli, "cmd_label", lambda a: got.update(teacher=a.teacher))
|
|
398
|
+
monkeypatch.setattr(cli, "_announce_learning", lambda min_new: got.update(announced=min_new))
|
|
399
|
+
monkeypatch.setattr(train, "missing_train_deps", lambda: [])
|
|
400
|
+
monkeypatch.setattr(cli, "_run_eval", lambda *a, **k: SimpleNamespace(
|
|
401
|
+
wer=0.1, term_recall=0.5, repetitions=0, per_clip=[]))
|
|
402
|
+
monkeypatch.setattr(cli, "_train", lambda ds, **k: got.update(k) or store.learn_dir() / "r")
|
|
403
|
+
monkeypatch.setattr(evaluate, "promotion_verdict", lambda c, b: (False, "table"))
|
|
404
|
+
return got
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _learn_config(**values) -> None:
|
|
408
|
+
for key, value in values.items():
|
|
409
|
+
config.set_value("learn", key, value)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def test_maintain_trains_what_learn_config_says(monkeypatch):
|
|
413
|
+
import argparse
|
|
414
|
+
|
|
415
|
+
from voiceio.cli import learn as cli
|
|
416
|
+
_learn_config(base="medium.en", teacher="large-v3", epochs=2, min_new=3)
|
|
417
|
+
got = _maintain_calls(monkeypatch)
|
|
418
|
+
cli.cmd_maintain(argparse.Namespace(min_new=None, base=None, epochs=None))
|
|
419
|
+
assert got == {"teacher": "large-v3", "announced": 3, "base": "medium.en", "epochs": 2}
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def test_maintain_config_min_new_gates_retraining(monkeypatch, capsys):
|
|
423
|
+
import argparse
|
|
424
|
+
|
|
425
|
+
from voiceio.cli import learn as cli
|
|
426
|
+
_learn_config(min_new=4)
|
|
427
|
+
got = _maintain_calls(monkeypatch)
|
|
428
|
+
cli.cmd_maintain(argparse.Namespace(min_new=None, base=None, epochs=None))
|
|
429
|
+
assert "base" not in got and "retraining waits for 4" in capsys.readouterr().out
|
|
430
|
+
assert got["teacher"] is None # empty: the default teacher
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def test_maintain_flags_override_learn_config(monkeypatch):
|
|
434
|
+
import argparse
|
|
435
|
+
|
|
436
|
+
from voiceio.cli import learn as cli
|
|
437
|
+
_learn_config(base="medium.en", epochs=2, min_new=100)
|
|
438
|
+
got = _maintain_calls(monkeypatch)
|
|
439
|
+
cli.cmd_maintain(argparse.Namespace(min_new=3, base="tiny", epochs=7))
|
|
440
|
+
assert (got["announced"], got["base"], got["epochs"]) == (3, "tiny", 7)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
@pytest.mark.parametrize("values, message", [
|
|
444
|
+
({"epochs": 0}, "epochs must be at least 1"),
|
|
445
|
+
({"min_new": 0}, "min_new must be at least 1"),
|
|
446
|
+
({"base": ""}, "base must name a model"),
|
|
447
|
+
])
|
|
448
|
+
def test_maintain_rejects_bad_learn_config(monkeypatch, values, message):
|
|
449
|
+
import argparse
|
|
450
|
+
|
|
451
|
+
from voiceio.cli import learn as cli
|
|
452
|
+
_learn_config(**values)
|
|
453
|
+
got = _maintain_calls(monkeypatch)
|
|
454
|
+
with pytest.raises(SystemExit, match=message):
|
|
455
|
+
cli.cmd_maintain(argparse.Namespace(min_new=None, base=None, epochs=None))
|
|
456
|
+
assert got == {} # refused before any work
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def test_english_only_base_needs_english(monkeypatch):
|
|
460
|
+
import argparse
|
|
461
|
+
|
|
462
|
+
from voiceio.cli import learn as cli
|
|
463
|
+
_learn_config(base="small.en")
|
|
464
|
+
config.set_value("model", "language", "de")
|
|
465
|
+
_maintain_calls(monkeypatch)
|
|
466
|
+
with pytest.raises(SystemExit, match="English-only"):
|
|
467
|
+
cli.cmd_maintain(argparse.Namespace(min_new=None, base=None, epochs=None))
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def test_english_only_bases_train_without_language_prefix():
|
|
471
|
+
assert train.HF_BASE["medium.en"] == "openai/whisper-medium.en"
|
|
472
|
+
cfg = train.TrainConfig(language="en")
|
|
473
|
+
assert train._prefix(51865, cfg) == {"language": "en", "task": "transcribe"}
|
|
474
|
+
assert train._prefix(51864, cfg) == {} # *.en: <|startoftranscript|> only
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def test_schedule_on_states_the_configured_threshold(monkeypatch, capsys):
|
|
478
|
+
import argparse
|
|
479
|
+
|
|
480
|
+
from voiceio import service
|
|
481
|
+
from voiceio.cli import learn as cli
|
|
482
|
+
_learn_config(min_new=20)
|
|
483
|
+
monkeypatch.setattr(train, "missing_train_deps", lambda: [])
|
|
484
|
+
monkeypatch.setattr(service, "install_learn_timer", lambda auto_promote: True)
|
|
485
|
+
monkeypatch.setattr(service, "learn_schedule_summary", lambda: "daily at 03:30")
|
|
486
|
+
cli.cmd_schedule(argparse.Namespace(state="on", at=None, every=None, auto_promote=False))
|
|
487
|
+
assert "retrains once 20+ clips are new" in capsys.readouterr().out
|
|
@@ -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.5.0"
|