python-voiceio 1.1.0__tar.gz → 1.2.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.2.0}/PKG-INFO +3 -3
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/README.md +2 -2
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/pyproject.toml +1 -1
- {python_voiceio-1.1.0 → python_voiceio-1.2.0/python_voiceio.egg-info}/PKG-INFO +3 -3
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_health.py +18 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_learn_pipeline.py +16 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_service.py +17 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_setup.py +15 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_transcriber.py +11 -0
- python_voiceio-1.2.0/voiceio/__init__.py +1 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/app.py +15 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/cli/learn.py +16 -2
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/cli/servicecmd.py +26 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/config.py +6 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/decoders/faster_whisper/__init__.py +3 -1
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/health.py +31 -1
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/learn/train.py +10 -6
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/setup/steps.py +20 -0
- python_voiceio-1.1.0/voiceio/__init__.py +0 -1
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/LICENSE +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/python_voiceio.egg-info/SOURCES.txt +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/python_voiceio.egg-info/requires.txt +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/setup.cfg +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_app_wiring.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_audio_quality.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_audio_source.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_backend_probes.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_cli.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_clipboard_read.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_commands.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_concurrency_lockdown.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_config.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_corrections.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_decoders.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_engine.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_evdev_uaccess.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_fallback.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_fcitx_bridge.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_hints.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_history.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_ibus_engine_limits.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_ibus_engine_listener.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_ibus_pending.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_ibus_pids.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_ibus_ping.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_ibus_session.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_ibus_textlimit.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_ibus_typer.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_learn_clean.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_learn_import.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_learn_metrics.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_learn_review.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_learn_targets.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_llm.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_llm_api.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_numbers.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_pipeline.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_platform.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_postcorrect.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_postprocess.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_prebuffer.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_prompt.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_retention.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_robustness.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_security_hardening.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_server.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_streaming.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_suggest.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_tokens.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_tts.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_typer_manager.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_vad.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_vocabulary.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_wordfreq.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_worker_decode.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/__main__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/audio_source.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/backends.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/cli/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/cli/configcmd.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/cli/corrections.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/cli/doctor.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/cli/history.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/cli/models.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/cli/uninstall.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/cli/vocab.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/clipboard_read.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/commands.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/consent.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/bias.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/decoders/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/decoders/base.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/decoders/catalog.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/decoders/lanes.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/decoders/sherpa.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/decoders/whispercpp.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/engine.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/pipeline.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/corrections.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/data/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/demo.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/display.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/feedback.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/hints.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/history.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/hotkeys/socket_backend.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/ibus/engine.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/ibus/install.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/ibus/pending.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/ibus/session.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/ibus/textlimit.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/learn/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/learn/augment.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/learn/clean.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/learn/evaluate.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/learn/importer.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/learn/metrics.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/learn/review.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/learn/store.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/learn/teacher.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/llm.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/llm_api.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/models/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/numbers.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/pidlock.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/platform.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/postcorrect.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/postprocess.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/privacy.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/prompt.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/recorder.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/retention.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/server/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/server/__main__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/server/app.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/server/config.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/server/runtime.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/service.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/setup/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/setup/configfile.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/setup/extras.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/setup/hotkey.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/setup/noninteractive.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/setup/speech.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/setup/system.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/setup/ui.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/streaming.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/suggest.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/tokens.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/tray/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/tray/_indicator.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/tray/_pystray.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/tts/base.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/tts/chain.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/tts/player.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/typers/base.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/typers/chain.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/typers/collecting.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/typers/fcitx_bridge.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/typers/ibus.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/typers/manager.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/typers/ydotool.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/ui.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/vad.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/vocab_stats.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/vocabulary.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/watchdog.py +0 -0
- {python_voiceio-1.1.0 → python_voiceio-1.2.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.2.0
|
|
4
4
|
Summary: Voice dictation for Linux. Speak → text, locally, instantly.
|
|
5
5
|
Author: Hugo Montenegro
|
|
6
6
|
License-Expression: MIT
|
|
@@ -292,7 +292,7 @@ There is a best-effort pynput path for both (hotkeys and typing, no live preview
|
|
|
292
292
|
| Hotkey does nothing on Wayland | `voiceio doctor --fix` installs the keyboard udev rule, or bind `voiceio toggle` in your compositor |
|
|
293
293
|
| `sounddevice is not installed` | You installed without the desktop extra: `pipx install --force 'python-voiceio[desktop]'` |
|
|
294
294
|
| 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
|
|
295
|
+
| 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
296
|
| Words come out wrong in a pattern | `voiceio learn eval --pipeline` and `voiceio corrections audit` show which stage or rule did it |
|
|
297
297
|
| Start over | `voiceio uninstall`, then `voiceio setup` |
|
|
298
298
|
|
|
@@ -300,7 +300,7 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
|
|
|
300
300
|
|
|
301
301
|
## Roadmap
|
|
302
302
|
|
|
303
|
-
- **A landing page** — voiceio as local, open-source, private dictation that learns from you.
|
|
303
|
+
- **A landing page** — voiceio as local, open-source, private dictation that learns from you. A draft is in [`site/index.html`](site/index.html), ready for GitHub Pages.
|
|
304
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
305
|
- **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
306
|
- **One model everywhere** — sync your fine-tune to your phone server and laptops, peer to peer.
|
|
@@ -239,7 +239,7 @@ There is a best-effort pynput path for both (hotkeys and typing, no live preview
|
|
|
239
239
|
| Hotkey does nothing on Wayland | `voiceio doctor --fix` installs the keyboard udev rule, or bind `voiceio toggle` in your compositor |
|
|
240
240
|
| `sounddevice is not installed` | You installed without the desktop extra: `pipx install --force 'python-voiceio[desktop]'` |
|
|
241
241
|
| 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
|
|
242
|
+
| 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
243
|
| Words come out wrong in a pattern | `voiceio learn eval --pipeline` and `voiceio corrections audit` show which stage or rule did it |
|
|
244
244
|
| Start over | `voiceio uninstall`, then `voiceio setup` |
|
|
245
245
|
|
|
@@ -247,7 +247,7 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
|
|
|
247
247
|
|
|
248
248
|
## Roadmap
|
|
249
249
|
|
|
250
|
-
- **A landing page** — voiceio as local, open-source, private dictation that learns from you.
|
|
250
|
+
- **A landing page** — voiceio as local, open-source, private dictation that learns from you. A draft is in [`site/index.html`](site/index.html), ready for GitHub Pages.
|
|
251
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
252
|
- **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
253
|
- **One model everywhere** — sync your fine-tune to your phone server and laptops, peer to peer.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-voiceio
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Voice dictation for Linux. Speak → text, locally, instantly.
|
|
5
5
|
Author: Hugo Montenegro
|
|
6
6
|
License-Expression: MIT
|
|
@@ -292,7 +292,7 @@ There is a best-effort pynput path for both (hotkeys and typing, no live preview
|
|
|
292
292
|
| Hotkey does nothing on Wayland | `voiceio doctor --fix` installs the keyboard udev rule, or bind `voiceio toggle` in your compositor |
|
|
293
293
|
| `sounddevice is not installed` | You installed without the desktop extra: `pipx install --force 'python-voiceio[desktop]'` |
|
|
294
294
|
| 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
|
|
295
|
+
| 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
296
|
| Words come out wrong in a pattern | `voiceio learn eval --pipeline` and `voiceio corrections audit` show which stage or rule did it |
|
|
297
297
|
| Start over | `voiceio uninstall`, then `voiceio setup` |
|
|
298
298
|
|
|
@@ -300,7 +300,7 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
|
|
|
300
300
|
|
|
301
301
|
## Roadmap
|
|
302
302
|
|
|
303
|
-
- **A landing page** — voiceio as local, open-source, private dictation that learns from you.
|
|
303
|
+
- **A landing page** — voiceio as local, open-source, private dictation that learns from you. A draft is in [`site/index.html`](site/index.html), ready for GitHub Pages.
|
|
304
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
305
|
- **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
306
|
- **One model everywhere** — sync your fine-tune to your phone server and laptops, peer to peer.
|
|
@@ -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))
|
|
@@ -230,11 +230,27 @@ def test_maintain_waits_for_enough_new_clips(monkeypatch, capsys):
|
|
|
230
230
|
store.add_label("new.wav", [Segment(0, 3, "a new one")], "teacher:fake")
|
|
231
231
|
|
|
232
232
|
monkeypatch.setattr(cli, "cmd_label", lambda args: None)
|
|
233
|
+
monkeypatch.setattr(train, "missing_train_deps", lambda: [])
|
|
233
234
|
monkeypatch.setattr(cli, "_train", lambda *a, **k: pytest.fail("must not retrain"))
|
|
234
235
|
cli.cmd_maintain(argparse.Namespace(min_new=5, base="small", epochs=1))
|
|
235
236
|
assert "1 new labeled clips since v0001" in capsys.readouterr().out
|
|
236
237
|
|
|
237
238
|
|
|
239
|
+
def test_maintain_without_train_extra_labels_and_exits_cleanly(monkeypatch, capsys):
|
|
240
|
+
"""A nightly timer must not fail every night, or spend an eval, over an
|
|
241
|
+
optional install."""
|
|
242
|
+
import argparse
|
|
243
|
+
|
|
244
|
+
from voiceio.cli import learn as cli
|
|
245
|
+
labeled = []
|
|
246
|
+
monkeypatch.setattr(cli, "cmd_label", lambda args: labeled.append(True))
|
|
247
|
+
monkeypatch.setattr(train, "missing_train_deps", lambda: ["torch", "peft"])
|
|
248
|
+
monkeypatch.setattr(store, "build_dataset", lambda: pytest.fail("must not build"))
|
|
249
|
+
monkeypatch.setattr(cli, "_run_eval", lambda *a, **k: pytest.fail("must not eval"))
|
|
250
|
+
cli.cmd_maintain(argparse.Namespace(min_new=5, base="small", epochs=1))
|
|
251
|
+
assert labeled and "python-voiceio[train]" in capsys.readouterr().out
|
|
252
|
+
|
|
253
|
+
|
|
238
254
|
def test_dev_mix_is_stable_per_row():
|
|
239
255
|
assert train._row_draws("a.wav") == train._row_draws("a.wav")
|
|
240
256
|
assert train._row_draws("a.wav") != train._row_draws("b.wav")
|
|
@@ -43,3 +43,20 @@ def test_learn_timer_runs_maintain_at_idle_priority(monkeypatch, tmp_path):
|
|
|
43
43
|
|
|
44
44
|
service.uninstall_learn_timer()
|
|
45
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
|
|
52
|
+
from voiceio.app import VoiceIO, _State
|
|
53
|
+
from voiceio.cli.servicecmd import wait_until_idle
|
|
54
|
+
|
|
55
|
+
assert wait_until_idle(timeout=1) # not running: no wait
|
|
56
|
+
vio = VoiceIO.__new__(VoiceIO)
|
|
57
|
+
vio._state = _State.RECORDING
|
|
58
|
+
assert config.STATE_PATH.read_text() == "recording"
|
|
59
|
+
threading.Timer(0.3, lambda: setattr(vio, "_state", _State.IDLE)).start()
|
|
60
|
+
assert wait_until_idle(timeout=5, poll=0.05) # waited for the dictation
|
|
61
|
+
vio._state = _State.FINALIZING
|
|
62
|
+
assert not wait_until_idle(timeout=0.2, poll=0.05) # gives up, says so
|
|
@@ -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.2.0"
|
|
@@ -36,6 +36,20 @@ 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
|
+
try: # best-effort: only `service restart` reads it
|
|
47
|
+
tmp = config.STATE_PATH.with_suffix(".tmp")
|
|
48
|
+
tmp.write_text(value.value)
|
|
49
|
+
tmp.replace(config.STATE_PATH)
|
|
50
|
+
except OSError:
|
|
51
|
+
pass
|
|
52
|
+
|
|
39
53
|
def __init__(self, cfg: config.Config):
|
|
40
54
|
self.cfg = cfg
|
|
41
55
|
|
|
@@ -713,4 +727,5 @@ class VoiceIO:
|
|
|
713
727
|
self._ibus.shutdown()
|
|
714
728
|
self._pid_fd.close()
|
|
715
729
|
PID_PATH.unlink(missing_ok=True)
|
|
730
|
+
config.STATE_PATH.unlink(missing_ok=True)
|
|
716
731
|
log.info("voiceio stopped")
|
|
@@ -63,6 +63,8 @@ def register(sub: argparse._SubParsersAction) -> None:
|
|
|
63
63
|
e.add_argument("--pipeline", action="store_true",
|
|
64
64
|
help="no decoding: score what each text stage (corrections, numbers, "
|
|
65
65
|
"cloud postcorrect…) did to past dictations")
|
|
66
|
+
e.add_argument("--beam", type=int, default=None,
|
|
67
|
+
help="beam width of the passes you keep (default: [model] beam_size)")
|
|
66
68
|
e.add_argument("--speed", type=float, default=1.0,
|
|
67
69
|
help="streaming replay speed; >1 is faster but skews latency")
|
|
68
70
|
e.set_defaults(func=cmd_eval)
|
|
@@ -334,12 +336,12 @@ def cmd_eval(args: argparse.Namespace) -> None:
|
|
|
334
336
|
if ds is None or not ds.exists():
|
|
335
337
|
raise SystemExit("no dataset — run `voiceio learn build` first")
|
|
336
338
|
_run_eval(args.model, ds, limit=args.limit, streaming=args.streaming, lanes=args.lanes,
|
|
337
|
-
freeze=args.freeze, speed=args.speed)
|
|
339
|
+
freeze=args.freeze, speed=args.speed, beam=args.beam)
|
|
338
340
|
|
|
339
341
|
|
|
340
342
|
def _run_eval(model: str | None, ds: Path, *, limit: int | None = None,
|
|
341
343
|
streaming: bool = False, lanes: int | None = None,
|
|
342
|
-
freeze: float | None = None, speed: float = 1.0):
|
|
344
|
+
freeze: float | None = None, speed: float = 1.0, beam: int | None = None):
|
|
343
345
|
"""Score `model` (default: the configured one) on `ds`; print and save."""
|
|
344
346
|
from voiceio.core.bias import BiasProvider
|
|
345
347
|
from voiceio.core.decoders import build_decoder
|
|
@@ -348,6 +350,8 @@ def _run_eval(model: str | None, ds: Path, *, limit: int | None = None,
|
|
|
348
350
|
cfg = _cfg()
|
|
349
351
|
if model:
|
|
350
352
|
cfg.model.name = model
|
|
353
|
+
if beam:
|
|
354
|
+
cfg.model.beam_size = beam
|
|
351
355
|
print(f"Loading {cfg.model.name}…", flush=True)
|
|
352
356
|
# Offline scoring needs one lane; streaming mirrors the live setup.
|
|
353
357
|
decoder = build_decoder(cfg.model, lanes=lanes if lanes is not None else
|
|
@@ -428,6 +432,12 @@ def cmd_maintain(args: argparse.Namespace) -> None:
|
|
|
428
432
|
_yield_cpu()
|
|
429
433
|
from voiceio.learn import evaluate
|
|
430
434
|
cmd_label(argparse.Namespace(teacher=None, limit=None, relabel=False))
|
|
435
|
+
from voiceio.learn import train
|
|
436
|
+
if missing := train.missing_train_deps():
|
|
437
|
+
# Labeling alone is worth it; retraining waits for the extra. Exit 0:
|
|
438
|
+
# a nightly timer must not fail every night over an optional install.
|
|
439
|
+
print(f"labeled; fine-tuning needs {', '.join(missing)}: {train.TRAIN_HINT}")
|
|
440
|
+
return
|
|
431
441
|
labels = store.load_labels()
|
|
432
442
|
labeled = {c.audio for c in store.clips() if c.audio in labels}
|
|
433
443
|
ds = store.latest_dataset()
|
|
@@ -457,6 +467,10 @@ def cmd_schedule(args: argparse.Namespace) -> None:
|
|
|
457
467
|
if not config.load().data.retain_audio:
|
|
458
468
|
print("note: [data] retain_audio is off, so there is nothing new to learn from;\n"
|
|
459
469
|
" turn it on with: voiceio config set data.retain_audio true")
|
|
470
|
+
from voiceio.learn import train
|
|
471
|
+
if missing := train.missing_train_deps():
|
|
472
|
+
print(f"note: until {', '.join(missing)} are installed it only labels new clips;\n"
|
|
473
|
+
f" to also retrain: {train.TRAIN_HINT}")
|
|
460
474
|
if not service.install_learn_timer():
|
|
461
475
|
raise SystemExit("could not enable the timer (needs systemd --user)")
|
|
462
476
|
print("on: every night ~03:30, `voiceio learn maintain` labels new dictation at idle "
|
|
@@ -10,6 +10,8 @@ def register(sub: argparse._SubParsersAction) -> None:
|
|
|
10
10
|
p.add_argument("action", nargs="?", default="status",
|
|
11
11
|
choices=["install", "uninstall", "start", "stop", "restart",
|
|
12
12
|
"status", "logs"])
|
|
13
|
+
p.add_argument("--now", action="store_true",
|
|
14
|
+
help="stop/restart: don't wait for a dictation in progress to finish")
|
|
13
15
|
p.add_argument("-f", "--follow", action="store_true", help="logs: follow")
|
|
14
16
|
p.add_argument("-n", "--lines", type=int, default=50, help="logs: line count")
|
|
15
17
|
p.set_defaults(func=_cmd_service)
|
|
@@ -61,6 +63,8 @@ def _cmd_service(args: argparse.Namespace) -> None:
|
|
|
61
63
|
sys.exit(1)
|
|
62
64
|
|
|
63
65
|
elif action in ("stop", "restart"):
|
|
66
|
+
if not getattr(args, "now", False):
|
|
67
|
+
wait_until_idle()
|
|
64
68
|
try:
|
|
65
69
|
subprocess.run(["systemctl", "--user", action, "voiceio.service"],
|
|
66
70
|
capture_output=True, timeout=10, check=True)
|
|
@@ -79,3 +83,25 @@ def _cmd_service(args: argparse.Namespace) -> None:
|
|
|
79
83
|
subprocess.call(cmd)
|
|
80
84
|
except KeyboardInterrupt:
|
|
81
85
|
pass
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def wait_until_idle(timeout: float = 300.0, poll: float = 0.25) -> bool:
|
|
89
|
+
"""Wait for the daemon to finish any dictation in progress (stopping it
|
|
90
|
+
mid-recording loses what was said). True once idle or not running."""
|
|
91
|
+
import time
|
|
92
|
+
|
|
93
|
+
from voiceio.config import STATE_PATH
|
|
94
|
+
deadline, told = time.monotonic() + timeout, False
|
|
95
|
+
while time.monotonic() < deadline:
|
|
96
|
+
try:
|
|
97
|
+
state = STATE_PATH.read_text().strip()
|
|
98
|
+
except OSError:
|
|
99
|
+
return True # not running (or pre-1.1): nothing to wait for
|
|
100
|
+
if state in ("idle", "error"):
|
|
101
|
+
return True
|
|
102
|
+
if not told:
|
|
103
|
+
print(f"Waiting for the dictation in progress to finish ({state})…", flush=True)
|
|
104
|
+
told = True
|
|
105
|
+
time.sleep(poll)
|
|
106
|
+
print("Still busy; going ahead. (--now skips the wait.)", file=sys.stderr)
|
|
107
|
+
return False
|
|
@@ -34,6 +34,9 @@ HISTORY_PATH = LOG_DIR / "history.jsonl"
|
|
|
34
34
|
RECORDINGS_DIR = LOG_DIR / "recordings"
|
|
35
35
|
LOG_PATH = LOG_DIR / "voiceio.log"
|
|
36
36
|
PID_PATH = LOG_DIR / "voiceio.pid"
|
|
37
|
+
# The daemon's state (idle/recording/finalizing), so `voiceio service
|
|
38
|
+
# restart` can wait for a dictation to finish instead of killing it.
|
|
39
|
+
STATE_PATH = Path(os.environ.get("XDG_RUNTIME_DIR", "/tmp")) / "voiceio.state"
|
|
37
40
|
# Intermediate pipeline data kept for debugging/profiling and future training:
|
|
38
41
|
# per-pass streaming decode traces and postcorrect before/after pairs. These
|
|
39
42
|
# live in their own JSONL files (not the rotating log) so they survive.
|
|
@@ -76,6 +79,9 @@ class ModelConfig:
|
|
|
76
79
|
# "modified_beam_search" (required for hotwords).
|
|
77
80
|
decoding: str = ""
|
|
78
81
|
vocabulary_file: str = "" # path to vocabulary.txt
|
|
82
|
+
# Beam width of the passes whose text you keep (freezes and the final
|
|
83
|
+
# tail); interim previews are always greedy. 1 is greedy: faster stop.
|
|
84
|
+
beam_size: int = 5
|
|
79
85
|
# Other languages you dictate in: a correction rule that would rewrite a
|
|
80
86
|
# real word in one of these is flagged (e.g. ["es"] protects "harina").
|
|
81
87
|
protect_languages: list[str] = field(default_factory=list)
|
{python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/decoders/faster_whisper/__init__.py
RENAMED
|
@@ -93,6 +93,8 @@ class Transcriber:
|
|
|
93
93
|
def __init__(self, cfg: ModelConfig):
|
|
94
94
|
self._cfg = cfg
|
|
95
95
|
self.name = f"faster-whisper:{cfg.name}"
|
|
96
|
+
if cfg.beam_size != 5: # scored separately from the default
|
|
97
|
+
self.name += f"@beam{cfg.beam_size}"
|
|
96
98
|
if _PROMPTLESS.search(cfg.name):
|
|
97
99
|
self.capabilities = Capabilities(bias=BiasMode.NONE)
|
|
98
100
|
self._proc: subprocess.Popen | None = None
|
|
@@ -205,7 +207,7 @@ class Transcriber:
|
|
|
205
207
|
|
|
206
208
|
audio = normalize_audio(audio)
|
|
207
209
|
audio_b64 = base64.b64encode(audio.tobytes()).decode("ascii")
|
|
208
|
-
req = {"audio_b64": audio_b64, "options": {"beam_size":
|
|
210
|
+
req = {"audio_b64": audio_b64, "options": {"beam_size": self._cfg.beam_size if final else 1}}
|
|
209
211
|
# Cap the combined prompt. initial_prompt (recent transcripts) and
|
|
210
212
|
# `context` (streaming's frozen-text tail) are both bounded in CHARS
|
|
211
213
|
# by their callers, but nothing bounded their SUM in tokens — and
|
|
@@ -217,6 +217,8 @@ def _check_features() -> FeatureGroups:
|
|
|
217
217
|
else:
|
|
218
218
|
groups.accuracy["Vocabulary"] = "none"
|
|
219
219
|
|
|
220
|
+
groups.accuracy.update(_learning_status(cfg))
|
|
221
|
+
|
|
220
222
|
if cfg.postcorrect.enabled:
|
|
221
223
|
model = cfg.postcorrect.model or cfg.cloud.model
|
|
222
224
|
groups.accuracy["Cloud postcorrect"] = f"on ({model}), adds up to {cfg.postcorrect.timeout_secs:g}s"
|
|
@@ -225,6 +227,30 @@ def _check_features() -> FeatureGroups:
|
|
|
225
227
|
return groups
|
|
226
228
|
|
|
227
229
|
|
|
230
|
+
def _learning_status(cfg) -> dict[str, str]:
|
|
231
|
+
"""Whether voiceio is learning your voice: the model in use, whether
|
|
232
|
+
recordings are kept to learn from, and whether it retrains nightly."""
|
|
233
|
+
from pathlib import Path
|
|
234
|
+
|
|
235
|
+
from voiceio import config, service
|
|
236
|
+
from voiceio.learn.store import learn_dir
|
|
237
|
+
out: dict[str, str] = {}
|
|
238
|
+
models = learn_dir() / "models"
|
|
239
|
+
name = cfg.model.name
|
|
240
|
+
if models in Path(name).expanduser().parents:
|
|
241
|
+
out["Voice model"] = f"fine-tuned on you ({Path(name).parent.name})"
|
|
242
|
+
else:
|
|
243
|
+
out["Voice model"] = f"stock ({name})"
|
|
244
|
+
if cfg.data.retain_audio:
|
|
245
|
+
n = sum(1 for _ in config.RECORDINGS_DIR.glob("*.wav")) \
|
|
246
|
+
if config.RECORDINGS_DIR.exists() else 0
|
|
247
|
+
out["Recordings"] = f"on ({n} kept to learn from)"
|
|
248
|
+
else:
|
|
249
|
+
out["Recordings"] = "off (nothing to learn from)"
|
|
250
|
+
out["Nightly learning"] = "on" if service.learn_timer_enabled() else "off"
|
|
251
|
+
return out
|
|
252
|
+
|
|
253
|
+
|
|
228
254
|
def _check_ibus() -> IBusStatus:
|
|
229
255
|
"""Run detailed IBus health checks."""
|
|
230
256
|
from pathlib import Path
|
|
@@ -325,6 +351,10 @@ def _accuracy_tips(accuracy: dict[str, str]) -> list[str]:
|
|
|
325
351
|
tips.append("voiceio corrections add WRONG RIGHT — fix a word that keeps being misheard")
|
|
326
352
|
if vocab == "none":
|
|
327
353
|
tips.append("Add names/jargon to ~/.config/voiceio/vocabulary.txt")
|
|
354
|
+
if accuracy.get("Recordings", "").startswith("off"):
|
|
355
|
+
tips.append("voiceio config set data.retain_audio true — keep recordings so voiceio can learn your voice")
|
|
356
|
+
elif accuracy.get("Nightly learning") == "off":
|
|
357
|
+
tips.append("voiceio learn schedule on — retrain on your voice every night")
|
|
328
358
|
return tips
|
|
329
359
|
|
|
330
360
|
|
|
@@ -391,7 +421,7 @@ def format_report(report: HealthReport) -> str:
|
|
|
391
421
|
# Accuracy / learning stack
|
|
392
422
|
if report.features.accuracy:
|
|
393
423
|
lines.append(_section("Accuracy"))
|
|
394
|
-
lines.append(f" {DIM}
|
|
424
|
+
lines.append(f" {DIM}What voiceio knows about your words, and whether it keeps learning.{RESET}")
|
|
395
425
|
for name, status in report.features.accuracy.items():
|
|
396
426
|
lines.append(_feature_line(name, status))
|
|
397
427
|
# Show actionable tips at the bottom
|
|
@@ -76,13 +76,17 @@ _DECODER_TARGETS = (r".*decoder\.layers\.\d+\.(self_attn\.(q_proj|k_proj|v_proj|
|
|
|
76
76
|
r"|encoder_attn\.(q_proj|out_proj)|fc1|fc2)")
|
|
77
77
|
|
|
78
78
|
|
|
79
|
+
TRAIN_HINT = "pip install 'python-voiceio[train]'"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def missing_train_deps() -> list[str]:
|
|
83
|
+
"""Training packages that aren't installed (the `train` extra)."""
|
|
84
|
+
import importlib.util
|
|
85
|
+
return [m for m in ("torch", "transformers", "peft") if importlib.util.find_spec(m) is None]
|
|
86
|
+
|
|
87
|
+
|
|
79
88
|
def require_train_deps() -> None:
|
|
80
|
-
missing =
|
|
81
|
-
for mod in ("torch", "transformers", "peft"):
|
|
82
|
-
try:
|
|
83
|
-
__import__(mod)
|
|
84
|
-
except ImportError:
|
|
85
|
-
missing.append(mod)
|
|
89
|
+
missing = missing_train_deps()
|
|
86
90
|
if missing:
|
|
87
91
|
raise RuntimeError(
|
|
88
92
|
f"training needs {', '.join(missing)}: pip install 'python-voiceio[train]'")
|
|
@@ -504,6 +504,8 @@ def _step_finalize(state: SetupState) -> None:
|
|
|
504
504
|
state.retain_audio = _ask_yn("Keep recordings so voiceio can learn your voice?",
|
|
505
505
|
default=state.cur.data.retain_audio if state.cur else True)
|
|
506
506
|
_write_config(**state.config_kwargs())
|
|
507
|
+
if state.retain_audio:
|
|
508
|
+
_offer_nightly_learning(state)
|
|
507
509
|
_setup_desktop_shortcut(state)
|
|
508
510
|
_install_autostart(state)
|
|
509
511
|
_verify_hotkey(state)
|
|
@@ -511,6 +513,24 @@ def _step_finalize(state: SetupState) -> None:
|
|
|
511
513
|
_start_service()
|
|
512
514
|
|
|
513
515
|
|
|
516
|
+
def _offer_nightly_learning(state: SetupState) -> None:
|
|
517
|
+
"""The recordings are only worth keeping if something learns from them."""
|
|
518
|
+
from voiceio import service
|
|
519
|
+
if not state.checks["is_linux"] or not service.has_systemd() or service.learn_timer_enabled():
|
|
520
|
+
return
|
|
521
|
+
print(f" {DIM}Every night at idle priority: transcribe new recordings with a bigger model,")
|
|
522
|
+
print(f" retrain once enough are new, and tell you if the result is better.{RESET}")
|
|
523
|
+
if not _ask_yn("Learn from them every night?", default=True):
|
|
524
|
+
return
|
|
525
|
+
if service.install_learn_timer():
|
|
526
|
+
print(f" {GREEN}✓{RESET} {DIM}Nightly learning on (voiceio learn schedule off to stop){RESET}")
|
|
527
|
+
from voiceio.learn import train
|
|
528
|
+
if missing := train.missing_train_deps():
|
|
529
|
+
print(f" {DIM}Retraining also needs {', '.join(missing)}: {train.TRAIN_HINT}{RESET}")
|
|
530
|
+
else:
|
|
531
|
+
print(f" {YELLOW}⚠{RESET} {DIM}Could not enable it; try: voiceio learn schedule on{RESET}")
|
|
532
|
+
|
|
533
|
+
|
|
514
534
|
def _setup_desktop_shortcut(state: SetupState) -> None:
|
|
515
535
|
"""Set up DE shortcut if on GNOME + socket backend (Linux only)."""
|
|
516
536
|
if not state.checks["is_linux"]:
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.1.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|