python-voiceio 1.0.0__tar.gz → 1.1.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.1.0}/PKG-INFO +8 -8
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/README.md +7 -7
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/pyproject.toml +1 -1
- {python_voiceio-1.0.0 → python_voiceio-1.1.0/python_voiceio.egg-info}/PKG-INFO +8 -8
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/python_voiceio.egg-info/SOURCES.txt +1 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_app_wiring.py +5 -5
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_concurrency_lockdown.py +2 -2
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_engine.py +1 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_postcorrect.py +12 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_postprocess.py +1 -1
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_retention.py +8 -8
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_robustness.py +20 -20
- python_voiceio-1.1.0/tests/test_service.py +45 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_setup.py +11 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_streaming.py +95 -98
- python_voiceio-1.1.0/voiceio/__init__.py +1 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/app.py +7 -7
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/cli/learn.py +29 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/cli/uninstall.py +3 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/config.py +7 -4
- python_voiceio-1.1.0/voiceio/display.py +150 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/service.py +71 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/setup/configfile.py +4 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/setup/noninteractive.py +3 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/setup/steps.py +6 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/streaming.py +28 -116
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/typers/manager.py +1 -1
- {python_voiceio-1.0.0 → python_voiceio-1.1.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.1.0}/LICENSE +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/python_voiceio.egg-info/requires.txt +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/setup.cfg +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_audio_quality.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_audio_source.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_backend_probes.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_cli.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_clipboard_read.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_commands.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_config.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_corrections.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_decoders.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_evdev_uaccess.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_fallback.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_fcitx_bridge.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_health.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_hints.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_history.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_ibus_engine_limits.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_ibus_engine_listener.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_ibus_pending.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_ibus_pids.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_ibus_ping.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_ibus_session.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_ibus_textlimit.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_ibus_typer.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_learn_clean.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_learn_import.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_learn_metrics.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_learn_pipeline.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_learn_review.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_learn_targets.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_llm.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_llm_api.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_numbers.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_pipeline.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_platform.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_prebuffer.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_prompt.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_security_hardening.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_server.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_suggest.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_tokens.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_transcriber.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_tts.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_typer_manager.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_vad.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_vocabulary.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_wordfreq.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_worker_decode.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/__main__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/audio_source.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/backends.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/cli/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/cli/configcmd.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/cli/corrections.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/cli/doctor.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/cli/history.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/cli/models.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/cli/servicecmd.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/cli/vocab.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/clipboard_read.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/commands.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/consent.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/core/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/core/bias.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/core/decoders/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/core/decoders/base.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/core/decoders/catalog.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/core/decoders/faster_whisper/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/core/decoders/lanes.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/core/decoders/sherpa.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/core/decoders/whispercpp.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/core/engine.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/core/pipeline.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/corrections.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/data/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/demo.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/feedback.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/health.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/hints.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/history.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/hotkeys/socket_backend.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/ibus/engine.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/ibus/install.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/ibus/pending.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/ibus/session.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/ibus/textlimit.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/learn/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/learn/augment.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/learn/clean.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/learn/evaluate.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/learn/importer.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/learn/metrics.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/learn/review.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/learn/store.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/learn/teacher.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/learn/train.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/llm.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/llm_api.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/models/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/numbers.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/pidlock.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/platform.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/postcorrect.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/postprocess.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/privacy.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/prompt.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/recorder.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/retention.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/server/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/server/__main__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/server/app.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/server/config.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/server/runtime.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/setup/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/setup/extras.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/setup/hotkey.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/setup/speech.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/setup/system.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/setup/ui.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/suggest.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/tokens.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/tray/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/tray/_indicator.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/tray/_pystray.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/tts/base.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/tts/chain.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/tts/player.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/typers/base.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/typers/chain.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/typers/collecting.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/typers/fcitx_bridge.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/typers/ibus.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/typers/ydotool.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/ui.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/vad.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/vocab_stats.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/vocabulary.py +0 -0
- {python_voiceio-1.0.0 → python_voiceio-1.1.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.1.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`.
|
|
@@ -301,7 +301,7 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
|
|
|
301
301
|
## Roadmap
|
|
302
302
|
|
|
303
303
|
- **A landing page** — voiceio as local, open-source, private dictation that learns from you.
|
|
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.
|
|
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`.
|
|
@@ -248,7 +248,7 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
|
|
|
248
248
|
## Roadmap
|
|
249
249
|
|
|
250
250
|
- **A landing page** — voiceio as local, open-source, private dictation that learns from you.
|
|
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.
|
|
254
254
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-voiceio
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.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`.
|
|
@@ -301,7 +301,7 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
|
|
|
301
301
|
## Roadmap
|
|
302
302
|
|
|
303
303
|
- **A landing page** — voiceio as local, open-source, private dictation that learns from you.
|
|
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.
|
|
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"
|
|
@@ -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.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from voiceio import service
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_install_symlinks_relinks_and_drops_legacy(monkeypatch, tmp_path):
|
|
8
|
+
venv_bin = tmp_path / "venv" / "bin"
|
|
9
|
+
venv_bin.mkdir(parents=True)
|
|
10
|
+
for name in service.SCRIPT_NAMES:
|
|
11
|
+
(venv_bin / name).write_text("#!/bin/sh\n")
|
|
12
|
+
old = tmp_path / "old"
|
|
13
|
+
old.mkdir()
|
|
14
|
+
local_bin = tmp_path / "local-bin"
|
|
15
|
+
local_bin.mkdir()
|
|
16
|
+
(local_bin / "voiceio").symlink_to(old / "voiceio") # stale install
|
|
17
|
+
(local_bin / "voiceio-doctor").symlink_to(old / "voiceio-doctor")
|
|
18
|
+
monkeypatch.setattr(service, "LOCAL_BIN", local_bin)
|
|
19
|
+
monkeypatch.setattr(sys, "prefix", str(tmp_path / "venv"))
|
|
20
|
+
monkeypatch.setattr(service, "_is_pipx_install", lambda: False)
|
|
21
|
+
monkeypatch.setattr(service, "_local_bin_on_path", lambda: True)
|
|
22
|
+
|
|
23
|
+
assert service.install_symlinks() == service.SCRIPT_NAMES
|
|
24
|
+
assert Path(local_bin / "voiceio").resolve() == (venv_bin / "voiceio").resolve()
|
|
25
|
+
assert not (local_bin / "voiceio-doctor").is_symlink()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_learn_timer_runs_maintain_at_idle_priority(monkeypatch, tmp_path):
|
|
29
|
+
calls = []
|
|
30
|
+
monkeypatch.setattr(service, "has_systemd", lambda: True)
|
|
31
|
+
monkeypatch.setattr(service, "_systemctl", lambda *a: calls.append(a) or True)
|
|
32
|
+
monkeypatch.setattr(service, "_find_voiceio_bin", lambda: "/opt/v/bin/voiceio")
|
|
33
|
+
monkeypatch.setattr(service, "SERVICE_DIR", tmp_path)
|
|
34
|
+
monkeypatch.setattr(service, "LEARN_SERVICE_PATH", tmp_path / service.LEARN_SERVICE_NAME)
|
|
35
|
+
monkeypatch.setattr(service, "LEARN_TIMER_PATH", tmp_path / service.LEARN_TIMER_NAME)
|
|
36
|
+
|
|
37
|
+
assert service.install_learn_timer()
|
|
38
|
+
unit = service.LEARN_SERVICE_PATH.read_text()
|
|
39
|
+
assert "ExecStart=/opt/v/bin/voiceio learn maintain" in unit
|
|
40
|
+
assert "CPUWeight=idle" in unit and "VOICEIO_IDLE_SCOPE=1" in unit
|
|
41
|
+
assert "Persistent=true" in service.LEARN_TIMER_PATH.read_text()
|
|
42
|
+
assert ("enable", "--now", service.LEARN_TIMER_NAME) in calls
|
|
43
|
+
|
|
44
|
+
service.uninstall_learn_timer()
|
|
45
|
+
assert not service.LEARN_TIMER_PATH.exists() and not service.LEARN_SERVICE_PATH.exists()
|
|
@@ -254,3 +254,14 @@ def test_write_config_with_api_key_records_consent_and_is_private(tmp_path, monk
|
|
|
254
254
|
configfile._record_cloud_consent()
|
|
255
255
|
assert consent.has_cloud_consent()
|
|
256
256
|
assert stat.S_IMODE(config.CONFIG_PATH.stat().st_mode) == 0o600
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_retention_is_written_only_when_asked(cfg_paths):
|
|
260
|
+
from voiceio import config
|
|
261
|
+
kw = dict(model="small", language="en", hotkey="ctrl+alt+v", method="auto",
|
|
262
|
+
streaming=True, backend="auto", quiet=True)
|
|
263
|
+
configfile._write_config(**kw)
|
|
264
|
+
assert "retain_audio" not in configfile._load_raw_config().get("data", {})
|
|
265
|
+
assert config.load(cfg_paths).data.retain_audio is False # off unless chosen
|
|
266
|
+
configfile._write_config(**kw, retain_audio=True)
|
|
267
|
+
assert config.load(cfg_paths).data.retain_audio is True
|