python-voiceio 1.1.0__tar.gz → 1.3.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.1.0/python_voiceio.egg-info → python_voiceio-1.3.0}/PKG-INFO +6 -5
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/README.md +5 -4
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/pyproject.toml +1 -1
- {python_voiceio-1.1.0 → python_voiceio-1.3.0/python_voiceio.egg-info}/PKG-INFO +6 -5
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_health.py +18 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_learn_pipeline.py +77 -3
- python_voiceio-1.3.0/tests/test_learn_review.py +44 -0
- python_voiceio-1.3.0/tests/test_service.py +93 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_setup.py +15 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_transcriber.py +11 -0
- python_voiceio-1.3.0/voiceio/__init__.py +1 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/app.py +23 -1
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/cli/learn.py +64 -18
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/cli/servicecmd.py +38 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/config.py +6 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/core/decoders/faster_whisper/__init__.py +3 -1
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/health.py +32 -1
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/learn/evaluate.py +15 -5
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/learn/review.py +30 -9
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/learn/train.py +10 -6
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/pidlock.py +18 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/service.py +19 -4
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/setup/steps.py +20 -0
- python_voiceio-1.1.0/tests/test_learn_review.py +0 -20
- python_voiceio-1.1.0/tests/test_service.py +0 -45
- python_voiceio-1.1.0/voiceio/__init__.py +0 -1
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/LICENSE +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/python_voiceio.egg-info/SOURCES.txt +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/python_voiceio.egg-info/requires.txt +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/setup.cfg +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_app_wiring.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_audio_quality.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_audio_source.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_backend_probes.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_cli.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_clipboard_read.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_commands.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_concurrency_lockdown.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_config.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_corrections.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_decoders.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_engine.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_evdev_uaccess.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_fallback.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_fcitx_bridge.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_hints.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_history.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_ibus_engine_limits.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_ibus_engine_listener.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_ibus_pending.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_ibus_pids.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_ibus_ping.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_ibus_session.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_ibus_textlimit.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_ibus_typer.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_learn_clean.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_learn_import.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_learn_metrics.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_learn_targets.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_llm.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_llm_api.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_numbers.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_pipeline.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_platform.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_postcorrect.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_postprocess.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_prebuffer.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_prompt.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_retention.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_robustness.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_security_hardening.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_server.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_streaming.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_suggest.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_tokens.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_tts.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_typer_manager.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_vad.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_vocabulary.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_wordfreq.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/tests/test_worker_decode.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/__main__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/audio_source.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/backends.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/cli/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/cli/configcmd.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/cli/corrections.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/cli/doctor.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/cli/history.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/cli/models.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/cli/uninstall.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/cli/vocab.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/clipboard_read.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/commands.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/consent.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/core/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/core/bias.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/core/decoders/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/core/decoders/base.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/core/decoders/catalog.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/core/decoders/lanes.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/core/decoders/sherpa.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/core/decoders/whispercpp.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/core/engine.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/core/pipeline.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/corrections.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/data/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/demo.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/display.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/feedback.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/hints.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/history.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/hotkeys/socket_backend.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/ibus/engine.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/ibus/install.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/ibus/pending.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/ibus/session.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/ibus/textlimit.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/learn/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/learn/augment.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/learn/clean.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/learn/importer.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/learn/metrics.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/learn/store.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/learn/teacher.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/llm.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/llm_api.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/models/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/numbers.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/platform.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/postcorrect.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/postprocess.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/privacy.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/prompt.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/recorder.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/retention.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/server/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/server/__main__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/server/app.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/server/config.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/server/runtime.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/setup/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/setup/configfile.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/setup/extras.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/setup/hotkey.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/setup/noninteractive.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/setup/speech.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/setup/system.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/setup/ui.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/streaming.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/suggest.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/tokens.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/tray/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/tray/_indicator.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/tray/_pystray.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/tts/base.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/tts/chain.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/tts/player.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/typers/base.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/typers/chain.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/typers/collecting.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/typers/fcitx_bridge.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/typers/ibus.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/typers/manager.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/typers/ydotool.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/ui.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/vad.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/vocab_stats.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/vocabulary.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/watchdog.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.3.0}/voiceio/wordfreq.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-voiceio
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Voice dictation for Linux. Speak → text, locally, instantly.
|
|
5
5
|
Author: Hugo Montenegro
|
|
6
6
|
License-Expression: MIT
|
|
@@ -182,7 +182,8 @@ Most dictation errors are proper nouns and jargon: `PostgreSQL` heard as `post g
|
|
|
182
182
|
voiceio learn import DIR --source phone # optional: add your own recordings from elsewhere
|
|
183
183
|
voiceio learn label # a stronger teacher model (large-v3-turbo) transcribes your clips
|
|
184
184
|
voiceio learn review # fix the teacher where it matters: your names and terms
|
|
185
|
-
voiceio learn review --test #
|
|
185
|
+
voiceio learn review --test # verify the held-out clips, so eval scores against the truth: it asks only
|
|
186
|
+
# where the teacher and your live decode disagree, quickest clips first
|
|
186
187
|
voiceio learn augment # synthesize extra examples of rare terms with local TTS
|
|
187
188
|
voiceio learn build # freeze a versioned train/dev/test dataset (the test split never trains)
|
|
188
189
|
voiceio learn eval # score the current model on your held-out clips: WER and term recall
|
|
@@ -191,6 +192,7 @@ voiceio learn train # LoRA fine-tune (needs the `train` extra; decoder-only
|
|
|
191
192
|
voiceio learn promote <run> # dictate with the fine-tune; `voiceio learn rollback` undoes it
|
|
192
193
|
voiceio learn maintain # later: label what's new, and once 50+ clips piled up, retrain and compare
|
|
193
194
|
voiceio learn schedule on # …or let it run `maintain` every night, and notify you when a model wins
|
|
195
|
+
# (--auto-promote: switch to a winner by itself; `learn rollback` undoes it)
|
|
194
196
|
```
|
|
195
197
|
|
|
196
198
|
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`.
|
|
@@ -292,7 +294,7 @@ There is a best-effort pynput path for both (hotkeys and typing, no live preview
|
|
|
292
294
|
| Hotkey does nothing on Wayland | `voiceio doctor --fix` installs the keyboard udev rule, or bind `voiceio toggle` in your compositor |
|
|
293
295
|
| `sounddevice is not installed` | You installed without the desktop extra: `pipx install --force 'python-voiceio[desktop]'` |
|
|
294
296
|
| evdev fails to build | Install a C toolchain and Python headers (see Quickstart), then reinstall |
|
|
295
|
-
| Too slow | `voiceio doctor` shows your real stop→text
|
|
297
|
+
| Too slow | `voiceio doctor` shows your real stop→text. Turn off cloud postcorrect; use `voiceio models use small` (or a GPU with `large-v3-turbo`); or narrow the beam of the passes you keep: on the author's 44 clips `[model] beam_size = 2` matched the default 5 (WER 4.9% vs 5.4%, vocabulary 28/32 vs 29/32) while decoding faster; greedy (`1`) lost vocabulary (26/32). Measure on yours: `voiceio learn eval --beam 2` |
|
|
296
298
|
| Words come out wrong in a pattern | `voiceio learn eval --pipeline` and `voiceio corrections audit` show which stage or rule did it |
|
|
297
299
|
| Start over | `voiceio uninstall`, then `voiceio setup` |
|
|
298
300
|
|
|
@@ -300,8 +302,7 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
|
|
|
300
302
|
|
|
301
303
|
## Roadmap
|
|
302
304
|
|
|
303
|
-
- **A landing page** — voiceio as local, open-source, private dictation that learns from you.
|
|
304
|
-
- **Hands-off promotion** — `learn schedule` already retrains nightly and tells you when a model wins; next is switching to it automatically, with one-command rollback.
|
|
305
|
+
- **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.
|
|
305
306
|
- **Unbounded vocabulary** — move to a decoder whose biasing has no token cap (see [model choice](CONTRIBUTING.md#model-choice-why-whisper-and-what-would-change-it)).
|
|
306
307
|
- **One model everywhere** — sync your fine-tune to your phone server and laptops, peer to peer.
|
|
307
308
|
|
|
@@ -129,7 +129,8 @@ Most dictation errors are proper nouns and jargon: `PostgreSQL` heard as `post g
|
|
|
129
129
|
voiceio learn import DIR --source phone # optional: add your own recordings from elsewhere
|
|
130
130
|
voiceio learn label # a stronger teacher model (large-v3-turbo) transcribes your clips
|
|
131
131
|
voiceio learn review # fix the teacher where it matters: your names and terms
|
|
132
|
-
voiceio learn review --test #
|
|
132
|
+
voiceio learn review --test # verify the held-out clips, so eval scores against the truth: it asks only
|
|
133
|
+
# where the teacher and your live decode disagree, quickest clips first
|
|
133
134
|
voiceio learn augment # synthesize extra examples of rare terms with local TTS
|
|
134
135
|
voiceio learn build # freeze a versioned train/dev/test dataset (the test split never trains)
|
|
135
136
|
voiceio learn eval # score the current model on your held-out clips: WER and term recall
|
|
@@ -138,6 +139,7 @@ voiceio learn train # LoRA fine-tune (needs the `train` extra; decoder-only
|
|
|
138
139
|
voiceio learn promote <run> # dictate with the fine-tune; `voiceio learn rollback` undoes it
|
|
139
140
|
voiceio learn maintain # later: label what's new, and once 50+ clips piled up, retrain and compare
|
|
140
141
|
voiceio learn schedule on # …or let it run `maintain` every night, and notify you when a model wins
|
|
142
|
+
# (--auto-promote: switch to a winner by itself; `learn rollback` undoes it)
|
|
141
143
|
```
|
|
142
144
|
|
|
143
145
|
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`.
|
|
@@ -239,7 +241,7 @@ There is a best-effort pynput path for both (hotkeys and typing, no live preview
|
|
|
239
241
|
| Hotkey does nothing on Wayland | `voiceio doctor --fix` installs the keyboard udev rule, or bind `voiceio toggle` in your compositor |
|
|
240
242
|
| `sounddevice is not installed` | You installed without the desktop extra: `pipx install --force 'python-voiceio[desktop]'` |
|
|
241
243
|
| evdev fails to build | Install a C toolchain and Python headers (see Quickstart), then reinstall |
|
|
242
|
-
| Too slow | `voiceio doctor` shows your real stop→text
|
|
244
|
+
| Too slow | `voiceio doctor` shows your real stop→text. Turn off cloud postcorrect; use `voiceio models use small` (or a GPU with `large-v3-turbo`); or narrow the beam of the passes you keep: on the author's 44 clips `[model] beam_size = 2` matched the default 5 (WER 4.9% vs 5.4%, vocabulary 28/32 vs 29/32) while decoding faster; greedy (`1`) lost vocabulary (26/32). Measure on yours: `voiceio learn eval --beam 2` |
|
|
243
245
|
| Words come out wrong in a pattern | `voiceio learn eval --pipeline` and `voiceio corrections audit` show which stage or rule did it |
|
|
244
246
|
| Start over | `voiceio uninstall`, then `voiceio setup` |
|
|
245
247
|
|
|
@@ -247,8 +249,7 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
|
|
|
247
249
|
|
|
248
250
|
## Roadmap
|
|
249
251
|
|
|
250
|
-
- **A landing page** — voiceio as local, open-source, private dictation that learns from you.
|
|
251
|
-
- **Hands-off promotion** — `learn schedule` already retrains nightly and tells you when a model wins; next is switching to it automatically, with one-command rollback.
|
|
252
|
+
- **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.
|
|
252
253
|
- **Unbounded vocabulary** — move to a decoder whose biasing has no token cap (see [model choice](CONTRIBUTING.md#model-choice-why-whisper-and-what-would-change-it)).
|
|
253
254
|
- **One model everywhere** — sync your fine-tune to your phone server and laptops, peer to peer.
|
|
254
255
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-voiceio
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Voice dictation for Linux. Speak → text, locally, instantly.
|
|
5
5
|
Author: Hugo Montenegro
|
|
6
6
|
License-Expression: MIT
|
|
@@ -182,7 +182,8 @@ Most dictation errors are proper nouns and jargon: `PostgreSQL` heard as `post g
|
|
|
182
182
|
voiceio learn import DIR --source phone # optional: add your own recordings from elsewhere
|
|
183
183
|
voiceio learn label # a stronger teacher model (large-v3-turbo) transcribes your clips
|
|
184
184
|
voiceio learn review # fix the teacher where it matters: your names and terms
|
|
185
|
-
voiceio learn review --test #
|
|
185
|
+
voiceio learn review --test # verify the held-out clips, so eval scores against the truth: it asks only
|
|
186
|
+
# where the teacher and your live decode disagree, quickest clips first
|
|
186
187
|
voiceio learn augment # synthesize extra examples of rare terms with local TTS
|
|
187
188
|
voiceio learn build # freeze a versioned train/dev/test dataset (the test split never trains)
|
|
188
189
|
voiceio learn eval # score the current model on your held-out clips: WER and term recall
|
|
@@ -191,6 +192,7 @@ voiceio learn train # LoRA fine-tune (needs the `train` extra; decoder-only
|
|
|
191
192
|
voiceio learn promote <run> # dictate with the fine-tune; `voiceio learn rollback` undoes it
|
|
192
193
|
voiceio learn maintain # later: label what's new, and once 50+ clips piled up, retrain and compare
|
|
193
194
|
voiceio learn schedule on # …or let it run `maintain` every night, and notify you when a model wins
|
|
195
|
+
# (--auto-promote: switch to a winner by itself; `learn rollback` undoes it)
|
|
194
196
|
```
|
|
195
197
|
|
|
196
198
|
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`.
|
|
@@ -292,7 +294,7 @@ There is a best-effort pynput path for both (hotkeys and typing, no live preview
|
|
|
292
294
|
| Hotkey does nothing on Wayland | `voiceio doctor --fix` installs the keyboard udev rule, or bind `voiceio toggle` in your compositor |
|
|
293
295
|
| `sounddevice is not installed` | You installed without the desktop extra: `pipx install --force 'python-voiceio[desktop]'` |
|
|
294
296
|
| evdev fails to build | Install a C toolchain and Python headers (see Quickstart), then reinstall |
|
|
295
|
-
| Too slow | `voiceio doctor` shows your real stop→text
|
|
297
|
+
| Too slow | `voiceio doctor` shows your real stop→text. Turn off cloud postcorrect; use `voiceio models use small` (or a GPU with `large-v3-turbo`); or narrow the beam of the passes you keep: on the author's 44 clips `[model] beam_size = 2` matched the default 5 (WER 4.9% vs 5.4%, vocabulary 28/32 vs 29/32) while decoding faster; greedy (`1`) lost vocabulary (26/32). Measure on yours: `voiceio learn eval --beam 2` |
|
|
296
298
|
| Words come out wrong in a pattern | `voiceio learn eval --pipeline` and `voiceio corrections audit` show which stage or rule did it |
|
|
297
299
|
| Start over | `voiceio uninstall`, then `voiceio setup` |
|
|
298
300
|
|
|
@@ -300,8 +302,7 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
|
|
|
300
302
|
|
|
301
303
|
## Roadmap
|
|
302
304
|
|
|
303
|
-
- **A landing page** — voiceio as local, open-source, private dictation that learns from you.
|
|
304
|
-
- **Hands-off promotion** — `learn schedule` already retrains nightly and tells you when a model wins; next is switching to it automatically, with one-command rollback.
|
|
305
|
+
- **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.
|
|
305
306
|
- **Unbounded vocabulary** — move to a decoder whose biasing has no token cap (see [model choice](CONTRIBUTING.md#model-choice-why-whisper-and-what-would-change-it)).
|
|
306
307
|
- **One model everywhere** — sync your fine-tune to your phone server and laptops, peer to peer.
|
|
307
308
|
|
|
@@ -105,3 +105,21 @@ def test_usage_decoder_counts_incidents_on_the_current_release(tmp_path, monkeyp
|
|
|
105
105
|
assert ok
|
|
106
106
|
assert finding.startswith(f"decoder: 0 hangs, 1 crashes in the last 7 days on {__version__}")
|
|
107
107
|
assert f"(1 more before {__version__})" in finding
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_learning_status_and_tips(monkeypatch):
|
|
111
|
+
from voiceio import config, service
|
|
112
|
+
from voiceio.health import _accuracy_tips, _learning_status
|
|
113
|
+
from voiceio.learn.store import learn_dir
|
|
114
|
+
monkeypatch.setattr(service, "learn_timer_enabled", lambda: False)
|
|
115
|
+
cfg = config.Config()
|
|
116
|
+
st = _learning_status(cfg)
|
|
117
|
+
assert st["Voice model"] == "stock (small)" and st["Recordings"].startswith("off")
|
|
118
|
+
assert any("retain_audio true" in t for t in _accuracy_tips(st))
|
|
119
|
+
|
|
120
|
+
cfg.data.retain_audio = True
|
|
121
|
+
cfg.model.name = str(learn_dir() / "models" / "v0001-small-x" / "ct2")
|
|
122
|
+
st = _learning_status(cfg)
|
|
123
|
+
assert st["Voice model"] == "fine-tuned on you (v0001-small-x)"
|
|
124
|
+
assert st["Recordings"].startswith("on (") and st["Nightly learning"] == "off"
|
|
125
|
+
assert any("learn schedule on" in t for t in _accuracy_tips(st))
|
|
@@ -132,14 +132,34 @@ def test_learn_jobs_reexec_into_an_idle_scope(monkeypatch):
|
|
|
132
132
|
|
|
133
133
|
|
|
134
134
|
def test_promotion_requires_a_winning_eval():
|
|
135
|
-
base = {"wer": 0.10, "term_recall": 0.40, "repetitions": 0}
|
|
136
|
-
better = {"wer": 0.101, "term_recall": 0.70, "repetitions": 0} # vocab win
|
|
137
|
-
worse = {"wer": 0.13, "term_recall": 0.70, "repetitions": 0}
|
|
135
|
+
base = {"wer": 0.10, "term_recall": 0.40, "repetitions": 0, "clips": 44}
|
|
136
|
+
better = {"wer": 0.101, "term_recall": 0.70, "repetitions": 0, "clips": 44} # vocab win
|
|
137
|
+
worse = {"wer": 0.13, "term_recall": 0.70, "repetitions": 0, "clips": 44}
|
|
138
138
|
assert evaluate.promotion_verdict(better, base)[0]
|
|
139
139
|
assert not evaluate.promotion_verdict(worse, base)[0]
|
|
140
140
|
assert not evaluate.promotion_verdict({**better, "repetitions": 2}, base)[0]
|
|
141
141
|
|
|
142
142
|
|
|
143
|
+
def test_no_promotion_without_evidence():
|
|
144
|
+
"""A crashed decode scores 0% WER; an empty test split scores nothing."""
|
|
145
|
+
base = {"wer": 0.10, "term_recall": 0.40, "repetitions": 0, "clips": 44}
|
|
146
|
+
perfect = {"wer": 0.0, "term_recall": 1.0, "repetitions": 0}
|
|
147
|
+
assert not evaluate.promotion_verdict({**perfect, "clips": 44, "failures": 3}, base)[0]
|
|
148
|
+
assert not evaluate.promotion_verdict({**perfect, "clips": 40}, base)[0] # other clips
|
|
149
|
+
assert not evaluate.promotion_verdict({**perfect, "clips": 2}, {**base, "clips": 2})[0]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_latest_finds_runs_at_the_configured_beam():
|
|
153
|
+
runs = store.learn_dir() / "runs"
|
|
154
|
+
runs.mkdir(parents=True)
|
|
155
|
+
for i, (dec, wer) in enumerate([("faster-whisper:small", 5), ("faster-whisper:small@beam2+draft", 2)]):
|
|
156
|
+
(runs / f"eval-2026010{i}.json").write_text(json.dumps(
|
|
157
|
+
{"decoder": dec, "dataset": "v0001", "wer": wer, "metrics": metrics.METRICS_VERSION}))
|
|
158
|
+
assert evaluate.latest("small", "v0001")["wer"] == 5
|
|
159
|
+
assert evaluate.latest("small", "v0001", beam=2)["wer"] == 2
|
|
160
|
+
assert evaluate.latest("small", "v0001", beam=1) is None
|
|
161
|
+
|
|
162
|
+
|
|
143
163
|
def test_latest_eval_matches_model_dataset_and_mode():
|
|
144
164
|
runs = store.learn_dir() / "runs"
|
|
145
165
|
runs.mkdir(parents=True)
|
|
@@ -230,11 +250,27 @@ def test_maintain_waits_for_enough_new_clips(monkeypatch, capsys):
|
|
|
230
250
|
store.add_label("new.wav", [Segment(0, 3, "a new one")], "teacher:fake")
|
|
231
251
|
|
|
232
252
|
monkeypatch.setattr(cli, "cmd_label", lambda args: None)
|
|
253
|
+
monkeypatch.setattr(train, "missing_train_deps", lambda: [])
|
|
233
254
|
monkeypatch.setattr(cli, "_train", lambda *a, **k: pytest.fail("must not retrain"))
|
|
234
255
|
cli.cmd_maintain(argparse.Namespace(min_new=5, base="small", epochs=1))
|
|
235
256
|
assert "1 new labeled clips since v0001" in capsys.readouterr().out
|
|
236
257
|
|
|
237
258
|
|
|
259
|
+
def test_maintain_without_train_extra_labels_and_exits_cleanly(monkeypatch, capsys):
|
|
260
|
+
"""A nightly timer must not fail every night, or spend an eval, over an
|
|
261
|
+
optional install."""
|
|
262
|
+
import argparse
|
|
263
|
+
|
|
264
|
+
from voiceio.cli import learn as cli
|
|
265
|
+
labeled = []
|
|
266
|
+
monkeypatch.setattr(cli, "cmd_label", lambda args: labeled.append(True))
|
|
267
|
+
monkeypatch.setattr(train, "missing_train_deps", lambda: ["torch", "peft"])
|
|
268
|
+
monkeypatch.setattr(store, "build_dataset", lambda: pytest.fail("must not build"))
|
|
269
|
+
monkeypatch.setattr(cli, "_run_eval", lambda *a, **k: pytest.fail("must not eval"))
|
|
270
|
+
cli.cmd_maintain(argparse.Namespace(min_new=5, base="small", epochs=1))
|
|
271
|
+
assert labeled and "python-voiceio[train]" in capsys.readouterr().out
|
|
272
|
+
|
|
273
|
+
|
|
238
274
|
def test_dev_mix_is_stable_per_row():
|
|
239
275
|
assert train._row_draws("a.wav") == train._row_draws("a.wav")
|
|
240
276
|
assert train._row_draws("a.wav") != train._row_draws("b.wav")
|
|
@@ -292,3 +328,41 @@ def test_wait_summary_p90_never_exceeds_max():
|
|
|
292
328
|
s = r.wait_summary()
|
|
293
329
|
p90 = float(s.split("p90 ")[1].split("s")[0])
|
|
294
330
|
assert p90 <= 6.83
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def test_maintain_auto_promote_switches_restarts_and_says_how_to_undo(monkeypatch):
|
|
334
|
+
import argparse
|
|
335
|
+
from types import SimpleNamespace
|
|
336
|
+
|
|
337
|
+
from voiceio import feedback, service
|
|
338
|
+
from voiceio.cli import learn as cli
|
|
339
|
+
from voiceio.learn import evaluate
|
|
340
|
+
for i in range(3):
|
|
341
|
+
_clip(f"{i}.wav", 3, "x")
|
|
342
|
+
store.add_label(f"{i}.wav", [Segment(0, 3, "hello there")], "teacher:fake")
|
|
343
|
+
run = store.learn_dir() / "models" / "v0001-small-x"
|
|
344
|
+
calls = []
|
|
345
|
+
monkeypatch.setattr(cli, "cmd_label", lambda a: None)
|
|
346
|
+
monkeypatch.setattr(train, "missing_train_deps", lambda: [])
|
|
347
|
+
monkeypatch.setattr(cli, "_run_eval", lambda *a, **k: SimpleNamespace(
|
|
348
|
+
wer=0.1, term_recall=0.5, repetitions=0, per_clip=[]))
|
|
349
|
+
monkeypatch.setattr(cli, "_train", lambda *a, **k: run)
|
|
350
|
+
monkeypatch.setattr(evaluate, "promotion_verdict", lambda c, b: (True, "table"))
|
|
351
|
+
monkeypatch.setattr(cli, "cmd_promote", lambda a: calls.append(("promote", a.run)))
|
|
352
|
+
monkeypatch.setattr(service, "is_installed", lambda: True)
|
|
353
|
+
monkeypatch.setattr(service, "restart_when_idle", lambda: calls.append(("restart",)) or True)
|
|
354
|
+
monkeypatch.setattr(feedback, "notify", lambda t, b, urgent=False: calls.append(("notify", b)))
|
|
355
|
+
|
|
356
|
+
cli.cmd_maintain(argparse.Namespace(min_new=1, base="small", epochs=1, auto_promote=True))
|
|
357
|
+
assert calls[:2] == [("promote", "v0001-small-x"), ("restart",)]
|
|
358
|
+
assert "voiceio learn rollback" in calls[2][1]
|
|
359
|
+
|
|
360
|
+
# The gate's own re-check refuses: no restart, no "switched" notification,
|
|
361
|
+
# and no crash (the nightly unit must not fail).
|
|
362
|
+
calls.clear()
|
|
363
|
+
|
|
364
|
+
def refuse(a):
|
|
365
|
+
raise SystemExit("evaluate both models first")
|
|
366
|
+
monkeypatch.setattr(cli, "cmd_promote", refuse)
|
|
367
|
+
cli.cmd_maintain(argparse.Namespace(min_new=1, base="small", epochs=1, auto_promote=True))
|
|
368
|
+
assert calls == []
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
def test_queue_all_covers_every_segment_of_unreviewed_clips():
|
|
4
|
+
from voiceio.learn import review
|
|
5
|
+
from voiceio.learn.store import Clip, Label, Segment
|
|
6
|
+
a = Clip("a.wav", 0, 4, "", "")
|
|
7
|
+
b = Clip("b.wav", 0, 4, "", "")
|
|
8
|
+
segs = (Segment(0, 2, "one"), Segment(2, 4, "two"))
|
|
9
|
+
labels = {"a.wav": Label("a.wav", segs, "teacher:x", 0),
|
|
10
|
+
"b.wav": Label("b.wav", segs, "human", 0)}
|
|
11
|
+
items = review.queue_all([a, b], labels)
|
|
12
|
+
assert [(i.clip.audio, i.index) for i in items] == [("a.wav", 0), ("a.wav", 1)]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_live_span_shows_the_live_decode_only_where_it_differs():
|
|
16
|
+
from voiceio.learn.review import live_span
|
|
17
|
+
live = "okay so we use kalchi daily and then more stuff"
|
|
18
|
+
assert live_span("We use Kalshi daily.", live) == "we use kalchi daily"
|
|
19
|
+
assert live_span("We use Kalshi daily.", "so we use Kalshi daily ok") is None
|
|
20
|
+
assert live_span("totally different words here", "nothing alike at all") is None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_gold_review_asks_only_where_the_models_disagree_cheapest_clip_first():
|
|
24
|
+
from voiceio.learn import review
|
|
25
|
+
from voiceio.learn.store import Clip, Label, Segment
|
|
26
|
+
segs = (Segment(0, 2, "we use Kalshi daily"), Segment(2, 4, "ship it today"))
|
|
27
|
+
two = Clip("two.wav", 0, 4, "we use kalchi daily ship it to day", "")
|
|
28
|
+
one = Clip("one.wav", 0, 4, "we use kalchi daily ship it today", "")
|
|
29
|
+
agree = Clip("agree.wav", 0, 4, "we use Kalshi daily ship it today", "")
|
|
30
|
+
labels = {c.audio: Label(c.audio, segs, "teacher:x", 0) for c in (two, one, agree)}
|
|
31
|
+
items = review.queue_all([two, one, agree], labels)
|
|
32
|
+
assert [(i.clip.audio, i.index) for i in items] == [
|
|
33
|
+
("one.wav", 0), # one answer completes a gold clip
|
|
34
|
+
("two.wav", 0), ("two.wav", 1),
|
|
35
|
+
] # agree.wav: nothing to ask
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_gold_review_counts_one_misheard_term_as_disagreement():
|
|
39
|
+
from voiceio.learn import review
|
|
40
|
+
from voiceio.learn.store import Clip, Label, Segment
|
|
41
|
+
segs = (Segment(0, 4, "we ship the Kalshi bot every single day"),)
|
|
42
|
+
clip = Clip("a.wav", 0, 4, "we ship the kalchi bot every single day", "")
|
|
43
|
+
items = review.queue_all([clip], {"a.wav": Label("a.wav", segs, "teacher:x", 0)})
|
|
44
|
+
assert len(items) == 1
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from voiceio import service
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_install_symlinks_relinks_and_drops_legacy(monkeypatch, tmp_path):
|
|
8
|
+
venv_bin = tmp_path / "venv" / "bin"
|
|
9
|
+
venv_bin.mkdir(parents=True)
|
|
10
|
+
for name in service.SCRIPT_NAMES:
|
|
11
|
+
(venv_bin / name).write_text("#!/bin/sh\n")
|
|
12
|
+
old = tmp_path / "old"
|
|
13
|
+
old.mkdir()
|
|
14
|
+
local_bin = tmp_path / "local-bin"
|
|
15
|
+
local_bin.mkdir()
|
|
16
|
+
(local_bin / "voiceio").symlink_to(old / "voiceio") # stale install
|
|
17
|
+
(local_bin / "voiceio-doctor").symlink_to(old / "voiceio-doctor")
|
|
18
|
+
monkeypatch.setattr(service, "LOCAL_BIN", local_bin)
|
|
19
|
+
monkeypatch.setattr(sys, "prefix", str(tmp_path / "venv"))
|
|
20
|
+
monkeypatch.setattr(service, "_is_pipx_install", lambda: False)
|
|
21
|
+
monkeypatch.setattr(service, "_local_bin_on_path", lambda: True)
|
|
22
|
+
|
|
23
|
+
assert service.install_symlinks() == service.SCRIPT_NAMES
|
|
24
|
+
assert Path(local_bin / "voiceio").resolve() == (venv_bin / "voiceio").resolve()
|
|
25
|
+
assert not (local_bin / "voiceio-doctor").is_symlink()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_learn_timer_runs_maintain_at_idle_priority(monkeypatch, tmp_path):
|
|
29
|
+
calls = []
|
|
30
|
+
monkeypatch.setattr(service, "has_systemd", lambda: True)
|
|
31
|
+
monkeypatch.setattr(service, "_systemctl", lambda *a: calls.append(a) or True)
|
|
32
|
+
monkeypatch.setattr(service, "_find_voiceio_bin", lambda: "/opt/v/bin/voiceio")
|
|
33
|
+
monkeypatch.setattr(service, "SERVICE_DIR", tmp_path)
|
|
34
|
+
monkeypatch.setattr(service, "LEARN_SERVICE_PATH", tmp_path / service.LEARN_SERVICE_NAME)
|
|
35
|
+
monkeypatch.setattr(service, "LEARN_TIMER_PATH", tmp_path / service.LEARN_TIMER_NAME)
|
|
36
|
+
|
|
37
|
+
assert service.install_learn_timer()
|
|
38
|
+
unit = service.LEARN_SERVICE_PATH.read_text()
|
|
39
|
+
assert "ExecStart=/opt/v/bin/voiceio learn maintain" in unit
|
|
40
|
+
assert "CPUWeight=idle" in unit and "VOICEIO_IDLE_SCOPE=1" in unit
|
|
41
|
+
assert "Persistent=true" in service.LEARN_TIMER_PATH.read_text()
|
|
42
|
+
assert ("enable", "--now", service.LEARN_TIMER_NAME) in calls
|
|
43
|
+
|
|
44
|
+
service.uninstall_learn_timer()
|
|
45
|
+
assert not service.LEARN_TIMER_PATH.exists() and not service.LEARN_SERVICE_PATH.exists()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_daemon_publishes_state_and_restart_waits_for_idle(monkeypatch):
|
|
49
|
+
import threading
|
|
50
|
+
|
|
51
|
+
from voiceio import config, pidlock
|
|
52
|
+
from voiceio.app import VoiceIO, _State
|
|
53
|
+
from voiceio.cli.servicecmd import wait_until_idle
|
|
54
|
+
|
|
55
|
+
vio = VoiceIO.__new__(VoiceIO)
|
|
56
|
+
vio._state = _State.RECORDING # not the lock holder:
|
|
57
|
+
assert not config.STATE_PATH.exists() # says nothing
|
|
58
|
+
vio._publishes_state = True
|
|
59
|
+
vio._state = _State.RECORDING
|
|
60
|
+
assert config.STATE_PATH.read_text() == "recording"
|
|
61
|
+
|
|
62
|
+
monkeypatch.setattr(pidlock, "daemon_alive", lambda path: False)
|
|
63
|
+
assert wait_until_idle(timeout=5) # dead daemon: stale file, no wait
|
|
64
|
+
monkeypatch.setattr(pidlock, "daemon_alive", lambda path: True)
|
|
65
|
+
threading.Timer(0.3, lambda: setattr(vio, "_state", _State.IDLE)).start()
|
|
66
|
+
assert wait_until_idle(timeout=5, poll=0.05) # waited for the dictation
|
|
67
|
+
vio._state = _State.FINALIZING # a hung finalize: short budget
|
|
68
|
+
assert not wait_until_idle(timeout=60, poll=0.05, finalizing_timeout=0.2)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_daemon_alive_probes_the_pid_lock(tmp_path):
|
|
72
|
+
import fcntl
|
|
73
|
+
|
|
74
|
+
from voiceio.pidlock import daemon_alive
|
|
75
|
+
path = tmp_path / "voiceio.pid"
|
|
76
|
+
assert not daemon_alive(path)
|
|
77
|
+
with open(path, "a") as held:
|
|
78
|
+
fcntl.flock(held, fcntl.LOCK_EX)
|
|
79
|
+
assert daemon_alive(path)
|
|
80
|
+
assert not daemon_alive(path)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_learn_timer_auto_promote_is_opt_in(monkeypatch, tmp_path):
|
|
84
|
+
monkeypatch.setattr(service, "has_systemd", lambda: True)
|
|
85
|
+
monkeypatch.setattr(service, "_systemctl", lambda *a: True)
|
|
86
|
+
monkeypatch.setattr(service, "_find_voiceio_bin", lambda: "voiceio")
|
|
87
|
+
monkeypatch.setattr(service, "LEARN_SERVICE_PATH", tmp_path / "s.service")
|
|
88
|
+
monkeypatch.setattr(service, "LEARN_TIMER_PATH", tmp_path / "s.timer")
|
|
89
|
+
service.install_learn_timer()
|
|
90
|
+
assert not service.learn_timer_auto_promotes()
|
|
91
|
+
service.install_learn_timer(auto_promote=True)
|
|
92
|
+
assert "learn maintain --auto-promote" in service.LEARN_SERVICE_PATH.read_text()
|
|
93
|
+
assert service.learn_timer_auto_promotes()
|
|
@@ -265,3 +265,18 @@ def test_retention_is_written_only_when_asked(cfg_paths):
|
|
|
265
265
|
assert config.load(cfg_paths).data.retain_audio is False # off unless chosen
|
|
266
266
|
configfile._write_config(**kw, retain_audio=True)
|
|
267
267
|
assert config.load(cfg_paths).data.retain_audio is True
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_setup_offers_nightly_learning_once(monkeypatch):
|
|
271
|
+
from voiceio import service
|
|
272
|
+
from voiceio.setup import steps
|
|
273
|
+
installed = []
|
|
274
|
+
monkeypatch.setattr(service, "has_systemd", lambda: True)
|
|
275
|
+
monkeypatch.setattr(service, "learn_timer_enabled", lambda: bool(installed))
|
|
276
|
+
monkeypatch.setattr(service, "install_learn_timer", lambda: installed.append(1) or True)
|
|
277
|
+
asked = []
|
|
278
|
+
monkeypatch.setattr(steps, "_ask_yn", lambda q, default=True: asked.append(q) or True)
|
|
279
|
+
state = steps.SetupState(checks={"is_linux": True})
|
|
280
|
+
steps._offer_nightly_learning(state)
|
|
281
|
+
steps._offer_nightly_learning(state) # already on: not asked again
|
|
282
|
+
assert installed == [1] and len(asked) == 1
|
|
@@ -313,3 +313,14 @@ def test_no_log_call_formats_the_transcript_itself():
|
|
|
313
313
|
and any(leaks(a) for a in node.args[1:])):
|
|
314
314
|
hits.append(f"{path.name}:{node.lineno}")
|
|
315
315
|
assert not hits, hits
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def test_non_default_beam_is_scored_under_its_own_name():
|
|
319
|
+
"""An eval at beam 1 must not stand in for the model's default score."""
|
|
320
|
+
mock_proc = MagicMock()
|
|
321
|
+
mock_proc.stdout.readline.return_value = "READY\n"
|
|
322
|
+
mock_proc.poll.return_value = None
|
|
323
|
+
with patch("voiceio.core.decoders.faster_whisper.subprocess.Popen", return_value=mock_proc):
|
|
324
|
+
from voiceio.core.decoders.faster_whisper import Transcriber
|
|
325
|
+
assert Transcriber(ModelConfig()).name == "faster-whisper:small"
|
|
326
|
+
assert Transcriber(ModelConfig(beam_size=1)).name == "faster-whisper:small@beam1"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.3.0"
|
|
@@ -36,6 +36,24 @@ class _State(enum.Enum):
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
class VoiceIO:
|
|
39
|
+
@property
|
|
40
|
+
def _state(self) -> _State:
|
|
41
|
+
return self.__state
|
|
42
|
+
|
|
43
|
+
@_state.setter
|
|
44
|
+
def _state(self, value: _State) -> None:
|
|
45
|
+
self.__state = value
|
|
46
|
+
# Only the instance holding the PID lock speaks for the daemon: a
|
|
47
|
+
# second launch must not overwrite the running one's state.
|
|
48
|
+
if not getattr(self, "_publishes_state", False):
|
|
49
|
+
return
|
|
50
|
+
try: # best-effort: only `service restart` reads it
|
|
51
|
+
tmp = config.STATE_PATH.with_suffix(".tmp")
|
|
52
|
+
tmp.write_text(value.value)
|
|
53
|
+
tmp.replace(config.STATE_PATH)
|
|
54
|
+
except OSError:
|
|
55
|
+
pass
|
|
56
|
+
|
|
39
57
|
def __init__(self, cfg: config.Config):
|
|
40
58
|
self.cfg = cfg
|
|
41
59
|
|
|
@@ -646,7 +664,7 @@ class VoiceIO:
|
|
|
646
664
|
self._typers.redetect_at_start()
|
|
647
665
|
|
|
648
666
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
649
|
-
self._pid_fd = open(PID_PATH, "w"
|
|
667
|
+
self._pid_fd = open(PID_PATH, "a") # not "w": the running daemon's PID stays
|
|
650
668
|
try:
|
|
651
669
|
from voiceio.pidlock import lock_pid_file
|
|
652
670
|
lock_pid_file(self._pid_fd)
|
|
@@ -655,8 +673,11 @@ class VoiceIO:
|
|
|
655
673
|
log.error("Another voiceio instance is already running")
|
|
656
674
|
print("voiceio is already running. Stop it first: voiceio service stop")
|
|
657
675
|
return
|
|
676
|
+
self._pid_fd.truncate(0)
|
|
658
677
|
self._pid_fd.write(str(os.getpid()))
|
|
659
678
|
self._pid_fd.flush()
|
|
679
|
+
self._publishes_state = True
|
|
680
|
+
self._state = self._state
|
|
660
681
|
|
|
661
682
|
self._typers.prepare()
|
|
662
683
|
|
|
@@ -713,4 +734,5 @@ class VoiceIO:
|
|
|
713
734
|
self._ibus.shutdown()
|
|
714
735
|
self._pid_fd.close()
|
|
715
736
|
PID_PATH.unlink(missing_ok=True)
|
|
737
|
+
config.STATE_PATH.unlink(missing_ok=True)
|
|
716
738
|
log.info("voiceio stopped")
|