python-voiceio 1.0.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.0.0/python_voiceio.egg-info → python_voiceio-1.2.0}/PKG-INFO +10 -10
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/README.md +9 -9
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/pyproject.toml +1 -1
- {python_voiceio-1.0.0 → python_voiceio-1.2.0/python_voiceio.egg-info}/PKG-INFO +10 -10
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/python_voiceio.egg-info/SOURCES.txt +1 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_app_wiring.py +5 -5
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_concurrency_lockdown.py +2 -2
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_engine.py +1 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_health.py +18 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_learn_pipeline.py +16 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_postcorrect.py +12 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_postprocess.py +1 -1
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_retention.py +8 -8
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_robustness.py +20 -20
- python_voiceio-1.2.0/tests/test_service.py +62 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_setup.py +26 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_streaming.py +95 -98
- {python_voiceio-1.0.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.0.0 → python_voiceio-1.2.0}/voiceio/app.py +22 -7
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/cli/learn.py +45 -2
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/cli/servicecmd.py +26 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/cli/uninstall.py +3 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/config.py +13 -4
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/core/decoders/faster_whisper/__init__.py +3 -1
- python_voiceio-1.2.0/voiceio/display.py +150 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/health.py +31 -1
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/learn/train.py +10 -6
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/service.py +71 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/setup/configfile.py +4 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/setup/noninteractive.py +3 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/setup/steps.py +26 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/streaming.py +28 -116
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/typers/manager.py +1 -1
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/watchdog.py +15 -15
- python_voiceio-1.0.0/tests/test_service.py +0 -25
- python_voiceio-1.0.0/voiceio/__init__.py +0 -1
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/LICENSE +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/python_voiceio.egg-info/requires.txt +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/setup.cfg +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_audio_quality.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_audio_source.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_backend_probes.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_cli.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_clipboard_read.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_commands.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_config.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_corrections.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_decoders.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_evdev_uaccess.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_fallback.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_fcitx_bridge.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_hints.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_history.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_ibus_engine_limits.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_ibus_engine_listener.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_ibus_pending.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_ibus_pids.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_ibus_ping.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_ibus_session.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_ibus_textlimit.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_ibus_typer.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_learn_clean.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_learn_import.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_learn_metrics.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_learn_review.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_learn_targets.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_llm.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_llm_api.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_numbers.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_pipeline.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_platform.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_prebuffer.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_prompt.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_security_hardening.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_server.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_suggest.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_tokens.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_tts.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_typer_manager.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_vad.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_vocabulary.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_wordfreq.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_worker_decode.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/__main__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/audio_source.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/backends.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/cli/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/cli/configcmd.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/cli/corrections.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/cli/doctor.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/cli/history.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/cli/models.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/cli/vocab.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/clipboard_read.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/commands.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/consent.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/core/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/core/bias.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/core/decoders/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/core/decoders/base.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/core/decoders/catalog.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/core/decoders/lanes.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/core/decoders/sherpa.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/core/decoders/whispercpp.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/core/engine.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/core/pipeline.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/corrections.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/data/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/demo.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/feedback.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/hints.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/history.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/hotkeys/socket_backend.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/ibus/engine.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/ibus/install.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/ibus/pending.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/ibus/session.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/ibus/textlimit.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/learn/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/learn/augment.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/learn/clean.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/learn/evaluate.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/learn/importer.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/learn/metrics.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/learn/review.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/learn/store.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/learn/teacher.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/llm.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/llm_api.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/models/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/numbers.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/pidlock.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/platform.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/postcorrect.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/postprocess.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/privacy.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/prompt.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/recorder.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/retention.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/server/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/server/__main__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/server/app.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/server/config.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/server/runtime.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/setup/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/setup/extras.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/setup/hotkey.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/setup/speech.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/setup/system.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/setup/ui.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/suggest.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/tokens.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/tray/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/tray/_indicator.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/tray/_pystray.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/tts/base.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/tts/chain.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/tts/player.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/typers/base.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/typers/chain.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/typers/collecting.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/typers/fcitx_bridge.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/typers/ibus.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/typers/ydotool.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/ui.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/vad.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/vocab_stats.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/vocabulary.py +0 -0
- {python_voiceio-1.0.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
|
|
@@ -140,7 +140,7 @@ uv run voiceio setup # also links voiceio into ~/.local/bin
|
|
|
140
140
|
|
|
141
141
|
## Your data
|
|
142
142
|
|
|
143
|
-
Everything voiceio keeps is in three directories (they follow `XDG_CONFIG_HOME`, `XDG_STATE_HOME` and `XDG_DATA_HOME`). Files are created `0600`, directories `0700`. Nothing is uploaded.
|
|
143
|
+
Everything voiceio keeps is in three directories (they follow `XDG_CONFIG_HOME`, `XDG_STATE_HOME` and `XDG_DATA_HOME`). Files are created `0600`, directories `0700`. Nothing is uploaded. Recordings, decode traces and window titles are **off unless you turn them on** — `voiceio setup` asks whether to keep recordings so voiceio can learn your voice. Logs record sizes and timings, never your words.
|
|
144
144
|
|
|
145
145
|
| Path | Holds | Retention |
|
|
146
146
|
|---|---|---|
|
|
@@ -150,14 +150,13 @@ Everything voiceio keeps is in three directories (they follow `XDG_CONFIG_HOME`,
|
|
|
150
150
|
| `vocab_stats.json` | Per-term usage counts used to rank the vocabulary | Rebuilt as you dictate |
|
|
151
151
|
| `corrections.json` | Find/replace rules (`post grass` → `PostgreSQL`) | — |
|
|
152
152
|
| `flagged.txt` | Words flagged with the "correct that" voice command, for review | — |
|
|
153
|
-
| `snapshots/` | Copies of corrections + vocabulary from before 1.0
|
|
153
|
+
| `snapshots/` | Copies of corrections + vocabulary from before 1.0 (safe to delete) | — |
|
|
154
154
|
| `consent.json` | Whether you allowed cloud LLM calls (text only) | — |
|
|
155
155
|
| `hints.json` | Which CLI tips you have already seen | — |
|
|
156
156
|
| **`~/.local/state/voiceio/`** | *What you said* | |
|
|
157
|
-
| `history.jsonl` | Every committed transcript: time, raw and final text
|
|
158
|
-
| `recordings/*.wav` | The audio of each utterance, for `voiceio learn` | `[data] retain_audio
|
|
159
|
-
| `streaming_trace.jsonl`, `postcorrect_pairs.jsonl` | Per-pass decode
|
|
160
|
-
| `corrections_audit.jsonl` | When each correction rule fired (shows which rules earn their keep) | — |
|
|
157
|
+
| `history.jsonl` | Every committed transcript: time, raw and final text (and the focused-window title with `[data] capture_context`) | `[history] enabled`, `max_entries`, `max_age_days` (0 = keep forever) |
|
|
158
|
+
| `recordings/*.wav` | The audio of each utterance, for `voiceio learn` — **opt-in** | `[data] retain_audio` (default off), `max_audio_mb` (default 4096), `min_free_gb` (default 5) |
|
|
159
|
+
| `streaming_trace.jsonl`, `postcorrect_pairs.jsonl` | Per-pass decode text and timings, LLM before/after pairs — **opt-in**, for debugging | `[data] capture_intermediates` (default off); each capped at 64 MB |
|
|
161
160
|
| `voiceio.log`, `worker.log`, `ibus-engine-stderr.log` | Logs | `voiceio.log` rotates at 2 MB × 3 |
|
|
162
161
|
| **`~/.local/share/voiceio/learn/`** | *Personalization corpus* (only exists once you run `voiceio learn`) | Never pruned; delete freely |
|
|
163
162
|
| `labels.jsonl` | Reference transcripts from the teacher model or your review | — |
|
|
@@ -191,6 +190,7 @@ voiceio learn eval # score the current model on your held-out clips: WER a
|
|
|
191
190
|
voiceio learn train # LoRA fine-tune (needs the `train` extra; decoder-only on CPU, --full on a GPU)
|
|
192
191
|
voiceio learn promote <run> # dictate with the fine-tune; `voiceio learn rollback` undoes it
|
|
193
192
|
voiceio learn maintain # later: label what's new, and once 50+ clips piled up, retrain and compare
|
|
193
|
+
voiceio learn schedule on # …or let it run `maintain` every night, and notify you when a model wins
|
|
194
194
|
```
|
|
195
195
|
|
|
196
196
|
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 +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,8 +300,8 @@ 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.
|
|
304
|
-
- **
|
|
303
|
+
- **A landing page** — voiceio as local, open-source, private dictation that learns from you. A draft is in [`site/index.html`](site/index.html), ready for GitHub Pages.
|
|
304
|
+
- **Hands-off promotion** — `learn schedule` already retrains nightly and tells you when a model wins; next is switching to it automatically, with one-command rollback.
|
|
305
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.
|
|
307
307
|
|
|
@@ -87,7 +87,7 @@ uv run voiceio setup # also links voiceio into ~/.local/bin
|
|
|
87
87
|
|
|
88
88
|
## Your data
|
|
89
89
|
|
|
90
|
-
Everything voiceio keeps is in three directories (they follow `XDG_CONFIG_HOME`, `XDG_STATE_HOME` and `XDG_DATA_HOME`). Files are created `0600`, directories `0700`. Nothing is uploaded.
|
|
90
|
+
Everything voiceio keeps is in three directories (they follow `XDG_CONFIG_HOME`, `XDG_STATE_HOME` and `XDG_DATA_HOME`). Files are created `0600`, directories `0700`. Nothing is uploaded. Recordings, decode traces and window titles are **off unless you turn them on** — `voiceio setup` asks whether to keep recordings so voiceio can learn your voice. Logs record sizes and timings, never your words.
|
|
91
91
|
|
|
92
92
|
| Path | Holds | Retention |
|
|
93
93
|
|---|---|---|
|
|
@@ -97,14 +97,13 @@ Everything voiceio keeps is in three directories (they follow `XDG_CONFIG_HOME`,
|
|
|
97
97
|
| `vocab_stats.json` | Per-term usage counts used to rank the vocabulary | Rebuilt as you dictate |
|
|
98
98
|
| `corrections.json` | Find/replace rules (`post grass` → `PostgreSQL`) | — |
|
|
99
99
|
| `flagged.txt` | Words flagged with the "correct that" voice command, for review | — |
|
|
100
|
-
| `snapshots/` | Copies of corrections + vocabulary from before 1.0
|
|
100
|
+
| `snapshots/` | Copies of corrections + vocabulary from before 1.0 (safe to delete) | — |
|
|
101
101
|
| `consent.json` | Whether you allowed cloud LLM calls (text only) | — |
|
|
102
102
|
| `hints.json` | Which CLI tips you have already seen | — |
|
|
103
103
|
| **`~/.local/state/voiceio/`** | *What you said* | |
|
|
104
|
-
| `history.jsonl` | Every committed transcript: time, raw and final text
|
|
105
|
-
| `recordings/*.wav` | The audio of each utterance, for `voiceio learn` | `[data] retain_audio
|
|
106
|
-
| `streaming_trace.jsonl`, `postcorrect_pairs.jsonl` | Per-pass decode
|
|
107
|
-
| `corrections_audit.jsonl` | When each correction rule fired (shows which rules earn their keep) | — |
|
|
104
|
+
| `history.jsonl` | Every committed transcript: time, raw and final text (and the focused-window title with `[data] capture_context`) | `[history] enabled`, `max_entries`, `max_age_days` (0 = keep forever) |
|
|
105
|
+
| `recordings/*.wav` | The audio of each utterance, for `voiceio learn` — **opt-in** | `[data] retain_audio` (default off), `max_audio_mb` (default 4096), `min_free_gb` (default 5) |
|
|
106
|
+
| `streaming_trace.jsonl`, `postcorrect_pairs.jsonl` | Per-pass decode text and timings, LLM before/after pairs — **opt-in**, for debugging | `[data] capture_intermediates` (default off); each capped at 64 MB |
|
|
108
107
|
| `voiceio.log`, `worker.log`, `ibus-engine-stderr.log` | Logs | `voiceio.log` rotates at 2 MB × 3 |
|
|
109
108
|
| **`~/.local/share/voiceio/learn/`** | *Personalization corpus* (only exists once you run `voiceio learn`) | Never pruned; delete freely |
|
|
110
109
|
| `labels.jsonl` | Reference transcripts from the teacher model or your review | — |
|
|
@@ -138,6 +137,7 @@ voiceio learn eval # score the current model on your held-out clips: WER a
|
|
|
138
137
|
voiceio learn train # LoRA fine-tune (needs the `train` extra; decoder-only on CPU, --full on a GPU)
|
|
139
138
|
voiceio learn promote <run> # dictate with the fine-tune; `voiceio learn rollback` undoes it
|
|
140
139
|
voiceio learn maintain # later: label what's new, and once 50+ clips piled up, retrain and compare
|
|
140
|
+
voiceio learn schedule on # …or let it run `maintain` every night, and notify you when a model wins
|
|
141
141
|
```
|
|
142
142
|
|
|
143
143
|
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 +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,8 +247,8 @@ 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.
|
|
251
|
-
- **
|
|
250
|
+
- **A landing page** — voiceio as local, open-source, private dictation that learns from you. A draft is in [`site/index.html`](site/index.html), ready for GitHub Pages.
|
|
251
|
+
- **Hands-off promotion** — `learn schedule` already retrains nightly and tells you when a model wins; next is switching to it automatically, with one-command rollback.
|
|
252
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.
|
|
254
254
|
|
|
@@ -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
|
|
@@ -140,7 +140,7 @@ uv run voiceio setup # also links voiceio into ~/.local/bin
|
|
|
140
140
|
|
|
141
141
|
## Your data
|
|
142
142
|
|
|
143
|
-
Everything voiceio keeps is in three directories (they follow `XDG_CONFIG_HOME`, `XDG_STATE_HOME` and `XDG_DATA_HOME`). Files are created `0600`, directories `0700`. Nothing is uploaded.
|
|
143
|
+
Everything voiceio keeps is in three directories (they follow `XDG_CONFIG_HOME`, `XDG_STATE_HOME` and `XDG_DATA_HOME`). Files are created `0600`, directories `0700`. Nothing is uploaded. Recordings, decode traces and window titles are **off unless you turn them on** — `voiceio setup` asks whether to keep recordings so voiceio can learn your voice. Logs record sizes and timings, never your words.
|
|
144
144
|
|
|
145
145
|
| Path | Holds | Retention |
|
|
146
146
|
|---|---|---|
|
|
@@ -150,14 +150,13 @@ Everything voiceio keeps is in three directories (they follow `XDG_CONFIG_HOME`,
|
|
|
150
150
|
| `vocab_stats.json` | Per-term usage counts used to rank the vocabulary | Rebuilt as you dictate |
|
|
151
151
|
| `corrections.json` | Find/replace rules (`post grass` → `PostgreSQL`) | — |
|
|
152
152
|
| `flagged.txt` | Words flagged with the "correct that" voice command, for review | — |
|
|
153
|
-
| `snapshots/` | Copies of corrections + vocabulary from before 1.0
|
|
153
|
+
| `snapshots/` | Copies of corrections + vocabulary from before 1.0 (safe to delete) | — |
|
|
154
154
|
| `consent.json` | Whether you allowed cloud LLM calls (text only) | — |
|
|
155
155
|
| `hints.json` | Which CLI tips you have already seen | — |
|
|
156
156
|
| **`~/.local/state/voiceio/`** | *What you said* | |
|
|
157
|
-
| `history.jsonl` | Every committed transcript: time, raw and final text
|
|
158
|
-
| `recordings/*.wav` | The audio of each utterance, for `voiceio learn` | `[data] retain_audio
|
|
159
|
-
| `streaming_trace.jsonl`, `postcorrect_pairs.jsonl` | Per-pass decode
|
|
160
|
-
| `corrections_audit.jsonl` | When each correction rule fired (shows which rules earn their keep) | — |
|
|
157
|
+
| `history.jsonl` | Every committed transcript: time, raw and final text (and the focused-window title with `[data] capture_context`) | `[history] enabled`, `max_entries`, `max_age_days` (0 = keep forever) |
|
|
158
|
+
| `recordings/*.wav` | The audio of each utterance, for `voiceio learn` — **opt-in** | `[data] retain_audio` (default off), `max_audio_mb` (default 4096), `min_free_gb` (default 5) |
|
|
159
|
+
| `streaming_trace.jsonl`, `postcorrect_pairs.jsonl` | Per-pass decode text and timings, LLM before/after pairs — **opt-in**, for debugging | `[data] capture_intermediates` (default off); each capped at 64 MB |
|
|
161
160
|
| `voiceio.log`, `worker.log`, `ibus-engine-stderr.log` | Logs | `voiceio.log` rotates at 2 MB × 3 |
|
|
162
161
|
| **`~/.local/share/voiceio/learn/`** | *Personalization corpus* (only exists once you run `voiceio learn`) | Never pruned; delete freely |
|
|
163
162
|
| `labels.jsonl` | Reference transcripts from the teacher model or your review | — |
|
|
@@ -191,6 +190,7 @@ voiceio learn eval # score the current model on your held-out clips: WER a
|
|
|
191
190
|
voiceio learn train # LoRA fine-tune (needs the `train` extra; decoder-only on CPU, --full on a GPU)
|
|
192
191
|
voiceio learn promote <run> # dictate with the fine-tune; `voiceio learn rollback` undoes it
|
|
193
192
|
voiceio learn maintain # later: label what's new, and once 50+ clips piled up, retrain and compare
|
|
193
|
+
voiceio learn schedule on # …or let it run `maintain` every night, and notify you when a model wins
|
|
194
194
|
```
|
|
195
195
|
|
|
196
196
|
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 +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,8 +300,8 @@ 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.
|
|
304
|
-
- **
|
|
303
|
+
- **A landing page** — voiceio as local, open-source, private dictation that learns from you. A draft is in [`site/index.html`](site/index.html), ready for GitHub Pages.
|
|
304
|
+
- **Hands-off promotion** — `learn schedule` already retrains nightly and tells you when a model wins; next is switching to it automatically, with one-command rollback.
|
|
305
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.
|
|
307
307
|
|
|
@@ -13,18 +13,18 @@ import pytest
|
|
|
13
13
|
from voiceio.config import Config
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def _make_vio(
|
|
16
|
+
def _make_vio(mock_decoder=None):
|
|
17
17
|
"""Create a VoiceIO instance with mocked backends."""
|
|
18
18
|
mock_hotkey = MagicMock()
|
|
19
19
|
mock_hotkey.name = "socket"
|
|
20
20
|
mock_typer = MagicMock()
|
|
21
21
|
mock_typer.name = "clipboard"
|
|
22
|
-
if
|
|
23
|
-
|
|
22
|
+
if mock_decoder is None:
|
|
23
|
+
mock_decoder = FakeDecoder()
|
|
24
24
|
|
|
25
25
|
with patch("voiceio.app.hotkey_chain.select", return_value=mock_hotkey), \
|
|
26
26
|
patch("voiceio.app.typer_chain.select", return_value=mock_typer), \
|
|
27
|
-
patch("voiceio.app.build_decoder", return_value=
|
|
27
|
+
patch("voiceio.app.build_decoder", return_value=mock_decoder), \
|
|
28
28
|
patch("voiceio.app.plat.detect") as mock_detect:
|
|
29
29
|
mock_detect.return_value = MagicMock(display_server="wayland", desktop="gnome")
|
|
30
30
|
|
|
@@ -39,7 +39,7 @@ def _make_vio(mock_transcriber=None):
|
|
|
39
39
|
mock_stream.stopped = False
|
|
40
40
|
vio.recorder._stream = mock_stream # skip real audio
|
|
41
41
|
vio.recorder._last_callback_time = time.monotonic() # healthy heartbeat
|
|
42
|
-
return vio, mock_typer,
|
|
42
|
+
return vio, mock_typer, mock_decoder
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
def _feed_audio(vio, chunks=20):
|
|
@@ -98,7 +98,7 @@ class TestGuardedSwaps:
|
|
|
98
98
|
from voiceio.app import _State
|
|
99
99
|
vio = _make_app()
|
|
100
100
|
vio._state = _State.RECORDING
|
|
101
|
-
vio.
|
|
101
|
+
vio.decoder.healthy.return_value = True
|
|
102
102
|
vio.recorder.stream_health = MagicMock(return_value=(True, ""))
|
|
103
103
|
vio.cfg.tray.enabled = False
|
|
104
104
|
vio._typers.health_upkeep = MagicMock()
|
|
@@ -153,7 +153,7 @@ class TestIBusGoodCitizen:
|
|
|
153
153
|
from voiceio.app import _State
|
|
154
154
|
vio = _make_app(typer_name="ibus")
|
|
155
155
|
vio._state = _State.IDLE
|
|
156
|
-
vio.
|
|
156
|
+
vio.decoder.healthy.return_value = True
|
|
157
157
|
vio.recorder.stream_health = MagicMock(return_value=(True, ""))
|
|
158
158
|
vio.cfg.tray.enabled = False
|
|
159
159
|
vio._typers.health_upkeep = MagicMock()
|
|
@@ -67,6 +67,7 @@ def test_commit_strips_prefix_learns_and_keeps_history():
|
|
|
67
67
|
|
|
68
68
|
from voiceio import config
|
|
69
69
|
eng, _ = _engine(voice_input_prefix="[voice]")
|
|
70
|
+
eng.cfg.data.retain_audio = True
|
|
70
71
|
stored = eng.commit("[voice] we deployed the Kalshi bot", raw="we deployed the kalshi bot",
|
|
71
72
|
duration=2.0, audio=np.zeros(16000, dtype=np.float32))
|
|
72
73
|
assert stored == "we deployed the Kalshi bot"
|
|
@@ -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")
|
|
@@ -17,6 +17,7 @@ def _cfg(*, enabled=True, api_key="test-key", min_words=4, model="",
|
|
|
17
17
|
)
|
|
18
18
|
cfg.output.remove_disfluencies = remove_disfluencies
|
|
19
19
|
cfg.cloud.api_key = api_key
|
|
20
|
+
cfg.data.capture_intermediates = True # most tests here check the pairs log
|
|
20
21
|
return cfg
|
|
21
22
|
|
|
22
23
|
|
|
@@ -364,3 +365,14 @@ def test_guard_rejects_meaning_changes(original, corrected, reason):
|
|
|
364
365
|
def test_guard_allows_filler_repeats_and_false_starts(original, corrected):
|
|
365
366
|
pc = PostCorrector(_cfg(remove_disfluencies=True))
|
|
366
367
|
assert pc._guard(original, corrected) == (True, "")
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def test_nothing_recorded_unless_opted_in(tmp_path, monkeypatch):
|
|
371
|
+
from voiceio import config as cfgmod
|
|
372
|
+
pairs = tmp_path / "pairs.jsonl"
|
|
373
|
+
monkeypatch.setattr(cfgmod, "POSTCORRECT_PAIRS_PATH", pairs)
|
|
374
|
+
cfg = _cfg()
|
|
375
|
+
cfg.data.capture_intermediates = False
|
|
376
|
+
with patch("voiceio.llm_api.chat", return_value="Crisp chat is the tool"):
|
|
377
|
+
PostCorrector(cfg).correct("Chris chat is the tool")
|
|
378
|
+
assert not pairs.exists()
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
from voiceio.config import Config
|
|
5
5
|
from voiceio.core.pipeline import Pipeline
|
|
6
6
|
from voiceio.postprocess import cleanup, strip_disfluencies
|
|
7
|
-
from voiceio.
|
|
7
|
+
from voiceio.display import _word_match_len
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def _run(text: str, *, final: bool = False, **output) -> tuple[str, bool]:
|
|
@@ -12,7 +12,7 @@ from voiceio.config import DataConfig
|
|
|
12
12
|
|
|
13
13
|
class TestSaveAudio:
|
|
14
14
|
def test_saves_wav(self, tmp_path):
|
|
15
|
-
cfg = DataConfig()
|
|
15
|
+
cfg = DataConfig(retain_audio=True)
|
|
16
16
|
audio = np.full(16000, 0.1, dtype=np.float32)
|
|
17
17
|
with patch.object(retention, "RECORDINGS_DIR", tmp_path):
|
|
18
18
|
name = retention.save_audio(audio, ts=1700000000.5, cfg=cfg)
|
|
@@ -29,13 +29,13 @@ class TestSaveAudio:
|
|
|
29
29
|
assert retention.save_audio(audio, ts=0, cfg=cfg) is None
|
|
30
30
|
|
|
31
31
|
def test_none_and_empty_audio(self, tmp_path):
|
|
32
|
-
cfg = DataConfig()
|
|
32
|
+
cfg = DataConfig(retain_audio=True)
|
|
33
33
|
with patch.object(retention, "RECORDINGS_DIR", tmp_path):
|
|
34
34
|
assert retention.save_audio(None, ts=0, cfg=cfg) is None
|
|
35
35
|
assert retention.save_audio(np.zeros(0, dtype=np.float32), ts=0, cfg=cfg) is None
|
|
36
36
|
|
|
37
37
|
def test_roundtrip_amplitude(self, tmp_path):
|
|
38
|
-
cfg = DataConfig()
|
|
38
|
+
cfg = DataConfig(retain_audio=True)
|
|
39
39
|
audio = np.full(1600, 0.5, dtype=np.float32)
|
|
40
40
|
with patch.object(retention, "RECORDINGS_DIR", tmp_path):
|
|
41
41
|
name = retention.save_audio(audio, ts=1700000001.0, cfg=cfg)
|
|
@@ -47,7 +47,7 @@ class TestSaveAudio:
|
|
|
47
47
|
class TestPrune:
|
|
48
48
|
def test_prunes_oldest_over_cap(self, tmp_path):
|
|
49
49
|
import os
|
|
50
|
-
cfg = DataConfig(max_audio_mb=1)
|
|
50
|
+
cfg = DataConfig(retain_audio=True, max_audio_mb=1)
|
|
51
51
|
# Three ~600KB files: total ~1.8MB > 1MB cap → oldest goes
|
|
52
52
|
for i, name in enumerate(["a.wav", "b.wav", "c.wav"]):
|
|
53
53
|
p = tmp_path / name
|
|
@@ -60,7 +60,7 @@ class TestPrune:
|
|
|
60
60
|
assert "c.wav" in remaining
|
|
61
61
|
|
|
62
62
|
def test_noop_under_cap(self, tmp_path):
|
|
63
|
-
cfg = DataConfig(max_audio_mb=100)
|
|
63
|
+
cfg = DataConfig(retain_audio=True, max_audio_mb=100)
|
|
64
64
|
(tmp_path / "a.wav").write_bytes(b"\0" * 1000)
|
|
65
65
|
with patch.object(retention, "RECORDINGS_DIR", tmp_path):
|
|
66
66
|
retention.prune(cfg)
|
|
@@ -122,7 +122,7 @@ class TestSaveTrace:
|
|
|
122
122
|
from voiceio import retention
|
|
123
123
|
from voiceio.config import DataConfig
|
|
124
124
|
monkeypatch.setattr(retention, "TRACES_PATH", tmp_path / "trace.jsonl")
|
|
125
|
-
retention.save_trace(DataConfig(), {"ts": 1, "passes": []})
|
|
125
|
+
retention.save_trace(DataConfig(capture_intermediates=True), {"ts": 1, "passes": []})
|
|
126
126
|
assert (tmp_path / "trace.jsonl").exists()
|
|
127
127
|
|
|
128
128
|
def test_skips_when_disabled(self, tmp_path, monkeypatch):
|
|
@@ -140,7 +140,7 @@ class TestDiskFloor:
|
|
|
140
140
|
from voiceio.config import DataConfig
|
|
141
141
|
monkeypatch.setattr(retention, "_free_gb", lambda p: 1.0)
|
|
142
142
|
name = retention.save_audio(
|
|
143
|
-
np.zeros(1600, dtype=np.float32), 0.0, DataConfig(min_free_gb=5.0),
|
|
143
|
+
np.zeros(1600, dtype=np.float32), 0.0, DataConfig(retain_audio=True, min_free_gb=5.0),
|
|
144
144
|
)
|
|
145
145
|
assert name is None
|
|
146
146
|
|
|
@@ -151,7 +151,7 @@ class TestDiskFloor:
|
|
|
151
151
|
monkeypatch.setattr(retention, "RECORDINGS_DIR", tmp_path)
|
|
152
152
|
monkeypatch.setattr(retention, "_free_gb", lambda p: 100.0)
|
|
153
153
|
name = retention.save_audio(
|
|
154
|
-
np.zeros(1600, dtype=np.float32), 0.0, DataConfig(min_free_gb=5.0),
|
|
154
|
+
np.zeros(1600, dtype=np.float32), 0.0, DataConfig(retain_audio=True, min_free_gb=5.0),
|
|
155
155
|
)
|
|
156
156
|
assert name is not None and (tmp_path / name).exists()
|
|
157
157
|
|
|
@@ -17,18 +17,18 @@ from voiceio.config import Config
|
|
|
17
17
|
# Helpers (same pattern as test_app_wiring.py)
|
|
18
18
|
# ---------------------------------------------------------------------------
|
|
19
19
|
|
|
20
|
-
def _make_vio(
|
|
20
|
+
def _make_vio(mock_decoder=None):
|
|
21
21
|
"""Create a VoiceIO instance with mocked backends."""
|
|
22
22
|
mock_hotkey = MagicMock()
|
|
23
23
|
mock_hotkey.name = "socket"
|
|
24
24
|
mock_typer = MagicMock()
|
|
25
25
|
mock_typer.name = "clipboard"
|
|
26
|
-
if
|
|
27
|
-
|
|
26
|
+
if mock_decoder is None:
|
|
27
|
+
mock_decoder = FakeDecoder()
|
|
28
28
|
|
|
29
29
|
with patch("voiceio.app.hotkey_chain.select", return_value=mock_hotkey), \
|
|
30
30
|
patch("voiceio.app.typer_chain.select", return_value=mock_typer), \
|
|
31
|
-
patch("voiceio.app.build_decoder", return_value=
|
|
31
|
+
patch("voiceio.app.build_decoder", return_value=mock_decoder), \
|
|
32
32
|
patch("voiceio.app.plat.detect") as mock_detect:
|
|
33
33
|
mock_detect.return_value = MagicMock(display_server="wayland", desktop="gnome")
|
|
34
34
|
|
|
@@ -40,7 +40,7 @@ def _make_vio(mock_transcriber=None):
|
|
|
40
40
|
mock_stream.stopped = False
|
|
41
41
|
vio.recorder._stream = mock_stream
|
|
42
42
|
vio.recorder._last_callback_time = time.monotonic()
|
|
43
|
-
return vio, mock_typer,
|
|
43
|
+
return vio, mock_typer, mock_decoder
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
def _feed_audio(vio, chunks=20):
|
|
@@ -421,7 +421,7 @@ class TestHealthWatchdogAudioBackoff:
|
|
|
421
421
|
vio, _, _ = _make_vio()
|
|
422
422
|
vio.recorder.stream_health = MagicMock(return_value=(False, "stream not active"))
|
|
423
423
|
vio.recorder.reopen_stream = MagicMock()
|
|
424
|
-
vio.
|
|
424
|
+
vio.decoder.healthy = MagicMock(return_value=True)
|
|
425
425
|
|
|
426
426
|
with patch("voiceio.watchdog.tray"):
|
|
427
427
|
vio._watchdog.check()
|
|
@@ -434,7 +434,7 @@ class TestHealthWatchdogAudioBackoff:
|
|
|
434
434
|
vio._watchdog._next_stream_retry = 0 # allow retry
|
|
435
435
|
vio.recorder.stream_health = MagicMock(return_value=(False, "stream not active"))
|
|
436
436
|
vio.recorder.reopen_stream = MagicMock() # succeeds (no exception)
|
|
437
|
-
vio.
|
|
437
|
+
vio.decoder.healthy = MagicMock(return_value=True)
|
|
438
438
|
|
|
439
439
|
with patch("voiceio.watchdog.tray"):
|
|
440
440
|
vio._watchdog.check()
|
|
@@ -448,7 +448,7 @@ class TestHealthWatchdogAudioBackoff:
|
|
|
448
448
|
vio._watchdog._next_stream_retry = 0
|
|
449
449
|
vio.recorder.stream_health = MagicMock(return_value=(False, "stream not active"))
|
|
450
450
|
vio.recorder.reopen_stream = MagicMock(side_effect=OSError("device gone"))
|
|
451
|
-
vio.
|
|
451
|
+
vio.decoder.healthy = MagicMock(return_value=True)
|
|
452
452
|
|
|
453
453
|
with patch("voiceio.watchdog.tray"):
|
|
454
454
|
vio._watchdog.check()
|
|
@@ -463,7 +463,7 @@ class TestHealthWatchdogAudioBackoff:
|
|
|
463
463
|
vio._watchdog._next_stream_retry = time.monotonic() + 999 # far future
|
|
464
464
|
vio.recorder.stream_health = MagicMock(return_value=(False, "stream not active"))
|
|
465
465
|
vio.recorder.reopen_stream = MagicMock()
|
|
466
|
-
vio.
|
|
466
|
+
vio.decoder.healthy = MagicMock(return_value=True)
|
|
467
467
|
|
|
468
468
|
with patch("voiceio.watchdog.tray"):
|
|
469
469
|
vio._watchdog.check()
|
|
@@ -476,7 +476,7 @@ class TestHealthWatchdogAudioBackoff:
|
|
|
476
476
|
vio._watchdog._stream_fail_count = 3
|
|
477
477
|
vio._watchdog._next_stream_retry = time.monotonic() + 100
|
|
478
478
|
vio.recorder.stream_health = MagicMock(return_value=(True, ""))
|
|
479
|
-
vio.
|
|
479
|
+
vio.decoder.healthy = MagicMock(return_value=True)
|
|
480
480
|
|
|
481
481
|
with patch("voiceio.watchdog.tray"):
|
|
482
482
|
vio._watchdog.check()
|
|
@@ -489,7 +489,7 @@ class TestHealthWatchdogAudioBackoff:
|
|
|
489
489
|
vio, _, _ = _make_vio()
|
|
490
490
|
vio.recorder.stream_health = MagicMock(return_value=(False, "stream not active"))
|
|
491
491
|
vio.recorder.reopen_stream = MagicMock(side_effect=OSError("device gone"))
|
|
492
|
-
vio.
|
|
492
|
+
vio.decoder.healthy = MagicMock(return_value=True)
|
|
493
493
|
|
|
494
494
|
delays = []
|
|
495
495
|
with patch("voiceio.watchdog.tray"):
|
|
@@ -551,7 +551,7 @@ class TestPreFlightStreamCheck:
|
|
|
551
551
|
class TestStreamingWorkerExceptionHandling:
|
|
552
552
|
def test_worker_catches_exception_and_continues(self):
|
|
553
553
|
"""Worker loop should catch exceptions in _transcribe_and_apply and keep running."""
|
|
554
|
-
|
|
554
|
+
mock_decoder = FakeDecoder()
|
|
555
555
|
mock_typer = MagicMock()
|
|
556
556
|
mock_typer.name = "clipboard"
|
|
557
557
|
mock_recorder = MagicMock()
|
|
@@ -561,7 +561,7 @@ class TestStreamingWorkerExceptionHandling:
|
|
|
561
561
|
from voiceio.streaming import StreamingSession
|
|
562
562
|
|
|
563
563
|
session = StreamingSession(
|
|
564
|
-
|
|
564
|
+
mock_decoder, mock_typer, mock_recorder,
|
|
565
565
|
generation=0,
|
|
566
566
|
)
|
|
567
567
|
|
|
@@ -593,8 +593,8 @@ class TestStreamingWorkerExceptionHandling:
|
|
|
593
593
|
|
|
594
594
|
def test_final_transcription_exception_does_not_crash(self):
|
|
595
595
|
"""Exception during final transcription should be caught."""
|
|
596
|
-
|
|
597
|
-
|
|
596
|
+
mock_decoder = FakeDecoder()
|
|
597
|
+
mock_decoder.transcribe.side_effect = RuntimeError("model crash")
|
|
598
598
|
mock_typer = MagicMock()
|
|
599
599
|
mock_typer.name = "clipboard"
|
|
600
600
|
mock_recorder = MagicMock()
|
|
@@ -604,7 +604,7 @@ class TestStreamingWorkerExceptionHandling:
|
|
|
604
604
|
from voiceio.streaming import StreamingSession
|
|
605
605
|
|
|
606
606
|
session = StreamingSession(
|
|
607
|
-
|
|
607
|
+
mock_decoder, mock_typer, mock_recorder,
|
|
608
608
|
generation=0,
|
|
609
609
|
)
|
|
610
610
|
|
|
@@ -629,7 +629,7 @@ class TestTrayWatchdog:
|
|
|
629
629
|
vio, _, _ = _make_vio()
|
|
630
630
|
vio.cfg.tray.enabled = True
|
|
631
631
|
vio.recorder.stream_health = MagicMock(return_value=(True, ""))
|
|
632
|
-
vio.
|
|
632
|
+
vio.decoder.healthy = MagicMock(return_value=True)
|
|
633
633
|
|
|
634
634
|
with patch("voiceio.watchdog.tray") as mock_tray:
|
|
635
635
|
mock_tray.is_alive.return_value = False
|
|
@@ -640,7 +640,7 @@ class TestTrayWatchdog:
|
|
|
640
640
|
vio, _, _ = _make_vio()
|
|
641
641
|
vio.cfg.tray.enabled = True
|
|
642
642
|
vio.recorder.stream_health = MagicMock(return_value=(True, ""))
|
|
643
|
-
vio.
|
|
643
|
+
vio.decoder.healthy = MagicMock(return_value=True)
|
|
644
644
|
|
|
645
645
|
with patch("voiceio.watchdog.tray") as mock_tray:
|
|
646
646
|
mock_tray.is_alive.return_value = True
|
|
@@ -651,7 +651,7 @@ class TestTrayWatchdog:
|
|
|
651
651
|
vio, _, _ = _make_vio()
|
|
652
652
|
vio.cfg.tray.enabled = False
|
|
653
653
|
vio.recorder.stream_health = MagicMock(return_value=(True, ""))
|
|
654
|
-
vio.
|
|
654
|
+
vio.decoder.healthy = MagicMock(return_value=True)
|
|
655
655
|
|
|
656
656
|
with patch("voiceio.watchdog.tray") as mock_tray:
|
|
657
657
|
vio._watchdog.check()
|
|
@@ -810,7 +810,7 @@ class TestBootRaceAndReProbe:
|
|
|
810
810
|
vio._typers.typer = MagicMock()
|
|
811
811
|
vio._typers.typer.name = "clipboard"
|
|
812
812
|
vio.recorder.stream_health = MagicMock(return_value=(True, ""))
|
|
813
|
-
vio.
|
|
813
|
+
vio.decoder.healthy = MagicMock(return_value=True)
|
|
814
814
|
vio.cfg.tray.enabled = False
|
|
815
815
|
|
|
816
816
|
# Env arrives; health check should re-import, re-detect, and upgrade.
|