python-voiceio 1.11.0__tar.gz → 1.12.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.11.0/python_voiceio.egg-info → python_voiceio-1.12.0}/PKG-INFO +4 -4
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/README.md +3 -3
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/pyproject.toml +1 -1
- {python_voiceio-1.11.0 → python_voiceio-1.12.0/python_voiceio.egg-info}/PKG-INFO +4 -4
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/python_voiceio.egg-info/SOURCES.txt +2 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_config.py +6 -1
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_decoders.py +0 -56
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_postprocess.py +13 -0
- python_voiceio-1.12.0/tests/test_sherpa.py +369 -0
- python_voiceio-1.12.0/voiceio/__init__.py +1 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/cli/learn.py +20 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/cli/models.py +85 -8
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/config.py +16 -6
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/core/decoders/__init__.py +32 -11
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/core/decoders/catalog.py +51 -4
- python_voiceio-1.12.0/voiceio/core/decoders/fetch.py +198 -0
- python_voiceio-1.12.0/voiceio/core/decoders/sherpa.py +304 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/health.py +26 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/learn/evaluate.py +8 -3
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/postprocess.py +20 -4
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/service.py +1 -1
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/setup/noninteractive.py +5 -4
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/setup/speech.py +42 -1
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/setup/steps.py +11 -7
- python_voiceio-1.11.0/voiceio/__init__.py +0 -1
- python_voiceio-1.11.0/voiceio/core/decoders/sherpa.py +0 -108
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/LICENSE +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/python_voiceio.egg-info/requires.txt +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/setup.cfg +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_app_wiring.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_audio_quality.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_audio_source.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_backend_probes.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_cancel.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_cli.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_clipboard_read.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_clipboard_typer.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_commands.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_concurrency_lockdown.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_context_terms.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_corrections.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_dead_mic.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_engine.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_evdev_uaccess.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_fallback.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_fcitx_bridge.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_feedback_stream.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_focus.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_health.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_hints.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_history.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_hotkey_modes.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_ibus_engine_limits.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_ibus_engine_listener.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_ibus_pending.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_ibus_pids.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_ibus_ping.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_ibus_session.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_ibus_textlimit.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_ibus_typer.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_idle_audio.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_learn_clean.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_learn_import.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_learn_language.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_learn_metrics.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_learn_pipeline.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_learn_quiet.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_learn_resume.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_learn_review.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_learn_targets.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_llm.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_llm_api.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_mic_gain.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_modifier_guard.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_numbers.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_paste_last.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_pipeline.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_platform.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_postcorrect.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_prebuffer.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_presence.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_prompt.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_quiet_audio.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_report.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_retention.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_robustness.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_security_hardening.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_server.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_service.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_setup.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_streaming.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_suggest.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_tokens.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_transcriber.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_tts.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_typer_manager.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_vad.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_vocabulary.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_wordfreq.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_worker_decode.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/__main__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/app.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/audio_source.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/backends.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/cli/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/cli/configcmd.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/cli/corrections.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/cli/doctor.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/cli/history.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/cli/servicecmd.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/cli/uninstall.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/cli/vocab.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/clipboard_read.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/commands.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/consent.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/context_terms.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/core/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/core/bias.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/core/decoders/base.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/core/decoders/faster_whisper/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/core/decoders/lanes.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/core/decoders/whispercpp.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/core/engine.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/core/pipeline.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/corrections.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/data/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/demo.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/display.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/feedback.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/focus.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/hints.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/history.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/hotkeys/socket_backend.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/ibus/engine.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/ibus/install.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/ibus/pending.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/ibus/session.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/ibus/textlimit.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/idle_audio.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/learn/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/learn/augment.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/learn/clean.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/learn/importer.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/learn/metrics.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/learn/quiet.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/learn/review.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/learn/store.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/learn/teacher.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/learn/train.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/llm.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/llm_api.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/mic_gain.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/models/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/numbers.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/pidlock.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/platform.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/postcorrect.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/power.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/presence.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/privacy.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/prompt.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/quiet_audio.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/recorder.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/report.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/retention.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/server/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/server/__main__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/server/app.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/server/config.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/server/runtime.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/setup/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/setup/configfile.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/setup/extras.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/setup/hotkey.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/setup/system.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/setup/ui.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/sounds/alert.wav +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/streaming.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/suggest.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/tokens.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/tray/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/tray/_indicator.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/tray/_pystray.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/tts/base.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/tts/chain.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/tts/player.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/base.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/chain.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/collecting.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/fcitx_bridge.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/ibus.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/manager.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/modifier_guard.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/paste.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/ydotool.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/ui.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/vad.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/vocab_stats.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/vocabulary.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/watchdog.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.12.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.12.0
|
|
4
4
|
Summary: Voice dictation for Linux. Speak → text, locally, instantly.
|
|
5
5
|
Author: Hugo Montenegro
|
|
6
6
|
License-Expression: MIT
|
|
@@ -65,7 +65,7 @@ Dynamic: license-file
|
|
|
65
65
|
**[Try it in your browser at voiceio.dev](https://voiceio.dev)**: no install, the model runs in the tab.
|
|
66
66
|
|
|
67
67
|
- **Types anywhere.** Press your hotkey, speak, and the words stream into the focused app with a live underlined preview (IBus), on Wayland and X11: GNOME, KDE, Hyprland, sway, i3.
|
|
68
|
-
- **Runs on your machine.** Whisper decodes your speech locally. No account, no server, no telemetry; logs never contain your words.
|
|
68
|
+
- **Runs on your machine.** Whisper, or NVIDIA's Parakeet (about 2x faster, with every name you use as a hotword), decodes your speech locally. No account, no server, no telemetry; logs never contain your words.
|
|
69
69
|
- **Learns your words.** Keep your recordings (opt-in, on your disk) and voiceio fine-tunes its model on them while the machine is idle. It switches only when the new model wins on clips it never trained on. On the author's voice: 12.1% → 4.9% word errors after one night on a laptop CPU.
|
|
70
70
|
|
|
71
71
|
## Install
|
|
@@ -127,7 +127,7 @@ Everything lives in `~/.config/voiceio/config.toml`; [config.example.toml](confi
|
|
|
127
127
|
|
|
128
128
|
## Roadmap
|
|
129
129
|
|
|
130
|
-
- **
|
|
130
|
+
- **Parakeet by default for everyone**: it needs our sherpa-onnx fix ([#4084](https://github.com/k2-fsa/sherpa-onnx/pull/4084)) in an upstream release; until then `voiceio models sherpa && voiceio models use parakeet-tdt-0.6b-v2` installs a patched build ([why](CONTRIBUTING.md#model-choice-parakeet-where-it-can-be-biased-whisper-otherwise)).
|
|
131
131
|
- **GPU on AMD and Intel**: let setup run a Vulkan whisper.cpp server for you.
|
|
132
132
|
- **Typing without IBus on GNOME and KDE**: a libei backend.
|
|
133
133
|
- **Distro packages**: AUR (packaged, not yet published) and .rpm.
|
|
@@ -139,7 +139,7 @@ Open an issue before a large PR. [CONTRIBUTING.md](CONTRIBUTING.md) covers the a
|
|
|
139
139
|
|
|
140
140
|
## Credits
|
|
141
141
|
|
|
142
|
-
- Speech recognition: [faster-whisper](https://github.com/SYSTRAN/faster-whisper) and [CTranslate2](https://github.com/OpenNMT/CTranslate2), with OpenAI's [Whisper](https://github.com/openai/whisper) models. The in-browser demo runs [Moonshine](https://github.com/usefulsensors/moonshine) through [transformers.js](https://github.com/huggingface/transformers.js).
|
|
142
|
+
- Speech recognition: [faster-whisper](https://github.com/SYSTRAN/faster-whisper) and [CTranslate2](https://github.com/OpenNMT/CTranslate2), with OpenAI's [Whisper](https://github.com/openai/whisper) models; [sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx) with NVIDIA's [Parakeet-TDT](https://huggingface.co/nvidia/parakeet-tdt-0.6b-v2). The in-browser demo runs [Moonshine](https://github.com/usefulsensors/moonshine) through [transformers.js](https://github.com/huggingface/transformers.js).
|
|
143
143
|
- Ideas borrowed with thanks: modifier release, paste keys and media pausing from [Voxtype](https://github.com/peteonrails/voxtype); the landing page's look from Romario Kavin's [KOLlateral](https://github.com/RomarioKavin1/kollateral).
|
|
144
144
|
|
|
145
145
|
## License
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
**[Try it in your browser at voiceio.dev](https://voiceio.dev)**: no install, the model runs in the tab.
|
|
13
13
|
|
|
14
14
|
- **Types anywhere.** Press your hotkey, speak, and the words stream into the focused app with a live underlined preview (IBus), on Wayland and X11: GNOME, KDE, Hyprland, sway, i3.
|
|
15
|
-
- **Runs on your machine.** Whisper decodes your speech locally. No account, no server, no telemetry; logs never contain your words.
|
|
15
|
+
- **Runs on your machine.** Whisper, or NVIDIA's Parakeet (about 2x faster, with every name you use as a hotword), decodes your speech locally. No account, no server, no telemetry; logs never contain your words.
|
|
16
16
|
- **Learns your words.** Keep your recordings (opt-in, on your disk) and voiceio fine-tunes its model on them while the machine is idle. It switches only when the new model wins on clips it never trained on. On the author's voice: 12.1% → 4.9% word errors after one night on a laptop CPU.
|
|
17
17
|
|
|
18
18
|
## Install
|
|
@@ -74,7 +74,7 @@ Everything lives in `~/.config/voiceio/config.toml`; [config.example.toml](confi
|
|
|
74
74
|
|
|
75
75
|
## Roadmap
|
|
76
76
|
|
|
77
|
-
- **
|
|
77
|
+
- **Parakeet by default for everyone**: it needs our sherpa-onnx fix ([#4084](https://github.com/k2-fsa/sherpa-onnx/pull/4084)) in an upstream release; until then `voiceio models sherpa && voiceio models use parakeet-tdt-0.6b-v2` installs a patched build ([why](CONTRIBUTING.md#model-choice-parakeet-where-it-can-be-biased-whisper-otherwise)).
|
|
78
78
|
- **GPU on AMD and Intel**: let setup run a Vulkan whisper.cpp server for you.
|
|
79
79
|
- **Typing without IBus on GNOME and KDE**: a libei backend.
|
|
80
80
|
- **Distro packages**: AUR (packaged, not yet published) and .rpm.
|
|
@@ -86,7 +86,7 @@ Open an issue before a large PR. [CONTRIBUTING.md](CONTRIBUTING.md) covers the a
|
|
|
86
86
|
|
|
87
87
|
## Credits
|
|
88
88
|
|
|
89
|
-
- Speech recognition: [faster-whisper](https://github.com/SYSTRAN/faster-whisper) and [CTranslate2](https://github.com/OpenNMT/CTranslate2), with OpenAI's [Whisper](https://github.com/openai/whisper) models. The in-browser demo runs [Moonshine](https://github.com/usefulsensors/moonshine) through [transformers.js](https://github.com/huggingface/transformers.js).
|
|
89
|
+
- Speech recognition: [faster-whisper](https://github.com/SYSTRAN/faster-whisper) and [CTranslate2](https://github.com/OpenNMT/CTranslate2), with OpenAI's [Whisper](https://github.com/openai/whisper) models; [sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx) with NVIDIA's [Parakeet-TDT](https://huggingface.co/nvidia/parakeet-tdt-0.6b-v2). The in-browser demo runs [Moonshine](https://github.com/usefulsensors/moonshine) through [transformers.js](https://github.com/huggingface/transformers.js).
|
|
90
90
|
- Ideas borrowed with thanks: modifier release, paste keys and media pausing from [Voxtype](https://github.com/peteonrails/voxtype); the landing page's look from Romario Kavin's [KOLlateral](https://github.com/RomarioKavin1/kollateral).
|
|
91
91
|
|
|
92
92
|
## License
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-voiceio
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.12.0
|
|
4
4
|
Summary: Voice dictation for Linux. Speak → text, locally, instantly.
|
|
5
5
|
Author: Hugo Montenegro
|
|
6
6
|
License-Expression: MIT
|
|
@@ -65,7 +65,7 @@ Dynamic: license-file
|
|
|
65
65
|
**[Try it in your browser at voiceio.dev](https://voiceio.dev)**: no install, the model runs in the tab.
|
|
66
66
|
|
|
67
67
|
- **Types anywhere.** Press your hotkey, speak, and the words stream into the focused app with a live underlined preview (IBus), on Wayland and X11: GNOME, KDE, Hyprland, sway, i3.
|
|
68
|
-
- **Runs on your machine.** Whisper decodes your speech locally. No account, no server, no telemetry; logs never contain your words.
|
|
68
|
+
- **Runs on your machine.** Whisper, or NVIDIA's Parakeet (about 2x faster, with every name you use as a hotword), decodes your speech locally. No account, no server, no telemetry; logs never contain your words.
|
|
69
69
|
- **Learns your words.** Keep your recordings (opt-in, on your disk) and voiceio fine-tunes its model on them while the machine is idle. It switches only when the new model wins on clips it never trained on. On the author's voice: 12.1% → 4.9% word errors after one night on a laptop CPU.
|
|
70
70
|
|
|
71
71
|
## Install
|
|
@@ -127,7 +127,7 @@ Everything lives in `~/.config/voiceio/config.toml`; [config.example.toml](confi
|
|
|
127
127
|
|
|
128
128
|
## Roadmap
|
|
129
129
|
|
|
130
|
-
- **
|
|
130
|
+
- **Parakeet by default for everyone**: it needs our sherpa-onnx fix ([#4084](https://github.com/k2-fsa/sherpa-onnx/pull/4084)) in an upstream release; until then `voiceio models sherpa && voiceio models use parakeet-tdt-0.6b-v2` installs a patched build ([why](CONTRIBUTING.md#model-choice-parakeet-where-it-can-be-biased-whisper-otherwise)).
|
|
131
131
|
- **GPU on AMD and Intel**: let setup run a Vulkan whisper.cpp server for you.
|
|
132
132
|
- **Typing without IBus on GNOME and KDE**: a libei backend.
|
|
133
133
|
- **Distro packages**: AUR (packaged, not yet published) and .rpm.
|
|
@@ -139,7 +139,7 @@ Open an issue before a large PR. [CONTRIBUTING.md](CONTRIBUTING.md) covers the a
|
|
|
139
139
|
|
|
140
140
|
## Credits
|
|
141
141
|
|
|
142
|
-
- Speech recognition: [faster-whisper](https://github.com/SYSTRAN/faster-whisper) and [CTranslate2](https://github.com/OpenNMT/CTranslate2), with OpenAI's [Whisper](https://github.com/openai/whisper) models. The in-browser demo runs [Moonshine](https://github.com/usefulsensors/moonshine) through [transformers.js](https://github.com/huggingface/transformers.js).
|
|
142
|
+
- Speech recognition: [faster-whisper](https://github.com/SYSTRAN/faster-whisper) and [CTranslate2](https://github.com/OpenNMT/CTranslate2), with OpenAI's [Whisper](https://github.com/openai/whisper) models; [sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx) with NVIDIA's [Parakeet-TDT](https://huggingface.co/nvidia/parakeet-tdt-0.6b-v2). The in-browser demo runs [Moonshine](https://github.com/usefulsensors/moonshine) through [transformers.js](https://github.com/huggingface/transformers.js).
|
|
143
143
|
- Ideas borrowed with thanks: modifier release, paste keys and media pausing from [Voxtype](https://github.com/peteonrails/voxtype); the landing page's look from Romario Kavin's [KOLlateral](https://github.com/RomarioKavin1/kollateral).
|
|
144
144
|
|
|
145
145
|
## License
|
|
@@ -72,6 +72,7 @@ tests/test_security_hardening.py
|
|
|
72
72
|
tests/test_server.py
|
|
73
73
|
tests/test_service.py
|
|
74
74
|
tests/test_setup.py
|
|
75
|
+
tests/test_sherpa.py
|
|
75
76
|
tests/test_streaming.py
|
|
76
77
|
tests/test_suggest.py
|
|
77
78
|
tests/test_tokens.py
|
|
@@ -144,6 +145,7 @@ voiceio/core/pipeline.py
|
|
|
144
145
|
voiceio/core/decoders/__init__.py
|
|
145
146
|
voiceio/core/decoders/base.py
|
|
146
147
|
voiceio/core/decoders/catalog.py
|
|
148
|
+
voiceio/core/decoders/fetch.py
|
|
147
149
|
voiceio/core/decoders/lanes.py
|
|
148
150
|
voiceio/core/decoders/sherpa.py
|
|
149
151
|
voiceio/core/decoders/whispercpp.py
|
|
@@ -98,7 +98,7 @@ def test_example_config_matches_defaults():
|
|
|
98
98
|
import dataclasses
|
|
99
99
|
from pathlib import Path
|
|
100
100
|
|
|
101
|
-
from voiceio.config import Config
|
|
101
|
+
from voiceio.config import Config, ModelConfig
|
|
102
102
|
example = tomllib.loads(
|
|
103
103
|
(Path(__file__).parent.parent / "config.example.toml").read_text())
|
|
104
104
|
cfg = Config()
|
|
@@ -107,6 +107,11 @@ def test_example_config_matches_defaults():
|
|
|
107
107
|
assert sec.name in example, f"[{sec.name}] missing from example"
|
|
108
108
|
for f in dataclasses.fields(defaults):
|
|
109
109
|
assert f.name in example[sec.name], f"{sec.name}.{f.name} missing"
|
|
110
|
+
if (sec.name, f.name) == ("model", "name"):
|
|
111
|
+
# "auto" resolves to the default decoder on construction
|
|
112
|
+
assert example["model"]["name"] == "auto"
|
|
113
|
+
assert ModelConfig(name="auto").name == defaults.name
|
|
114
|
+
continue
|
|
110
115
|
assert example[sec.name][f.name] == getattr(defaults, f.name), \
|
|
111
116
|
f"{sec.name}.{f.name}: example differs from default"
|
|
112
117
|
for name, section in example.items():
|
|
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import io
|
|
5
5
|
import json
|
|
6
|
-
import sys
|
|
7
6
|
import types
|
|
8
7
|
|
|
9
8
|
import numpy as np
|
|
@@ -54,61 +53,6 @@ def test_whispercpp_errors_are_decode_errors(monkeypatch):
|
|
|
54
53
|
dec.decode(np.zeros(1600, dtype=np.float32))
|
|
55
54
|
|
|
56
55
|
|
|
57
|
-
def _fake_sherpa(monkeypatch, created):
|
|
58
|
-
class Stream:
|
|
59
|
-
def accept_waveform(self, sr, a):
|
|
60
|
-
self.n = len(a)
|
|
61
|
-
|
|
62
|
-
@property
|
|
63
|
-
def result(self):
|
|
64
|
-
return types.SimpleNamespace(text=" hello world ")
|
|
65
|
-
|
|
66
|
-
class Rec:
|
|
67
|
-
def create_stream(self):
|
|
68
|
-
return Stream()
|
|
69
|
-
|
|
70
|
-
def decode_stream(self, s):
|
|
71
|
-
pass
|
|
72
|
-
|
|
73
|
-
class OfflineRecognizer:
|
|
74
|
-
@staticmethod
|
|
75
|
-
def from_transducer(**kw):
|
|
76
|
-
created.append(kw)
|
|
77
|
-
return Rec()
|
|
78
|
-
|
|
79
|
-
monkeypatch.setitem(sys.modules, "sherpa_onnx",
|
|
80
|
-
types.SimpleNamespace(OfflineRecognizer=OfflineRecognizer))
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def _model_dir(tmp_path):
|
|
84
|
-
for f in ("encoder.int8.onnx", "decoder.int8.onnx", "joiner.int8.onnx", "tokens.txt"):
|
|
85
|
-
(tmp_path / f).write_text("x")
|
|
86
|
-
return tmp_path
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def test_sherpa_greedy_declares_no_bias(monkeypatch, tmp_path):
|
|
90
|
-
created = []
|
|
91
|
-
_fake_sherpa(monkeypatch, created)
|
|
92
|
-
dec = build_decoder(ModelConfig(name=f"sherpa:{_model_dir(tmp_path)}"))
|
|
93
|
-
assert dec.capabilities.bias == BiasMode.NONE
|
|
94
|
-
assert dec.decode(np.zeros(16000, dtype=np.float32),
|
|
95
|
-
bias=Bias(hotwords="Kalshi")).text == "hello world"
|
|
96
|
-
assert "hotwords_file" not in created[-1]
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def test_sherpa_mbs_passes_unbounded_hotwords(monkeypatch, tmp_path):
|
|
100
|
-
created = []
|
|
101
|
-
_fake_sherpa(monkeypatch, created)
|
|
102
|
-
dec = build_decoder(ModelConfig(name=f"sherpa:{_model_dir(tmp_path)}",
|
|
103
|
-
decoding="modified_beam_search"))
|
|
104
|
-
assert dec.capabilities.bias == BiasMode.UNBOUNDED
|
|
105
|
-
dec.decode(np.zeros(16000, dtype=np.float32), bias=Bias(hotwords="Kalshi, HyperNEAT"))
|
|
106
|
-
kw = created[-1]
|
|
107
|
-
assert kw["decoding_method"] == "modified_beam_search"
|
|
108
|
-
with open(kw["hotwords_file"]) as f:
|
|
109
|
-
assert f.read().split() == ["Kalshi", "HyperNEAT"]
|
|
110
|
-
|
|
111
|
-
|
|
112
56
|
def test_lanes_share_the_thread_budget(monkeypatch):
|
|
113
57
|
import voiceio.core.decoders.faster_whisper as tr
|
|
114
58
|
from voiceio.core.decoders.faster_whisper.worker import auto_cpu_threads
|
|
@@ -70,6 +70,19 @@ class TestStripDisfluencies:
|
|
|
70
70
|
"First para.\n\nSecond para."
|
|
71
71
|
assert "\n\n" in strip_disfluencies("First para.\n\num\n\nSecond para.")
|
|
72
72
|
|
|
73
|
+
def test_filler_sentence_goes_with_its_full_stop(self):
|
|
74
|
+
# Parakeet writes a filler as a sentence of its own.
|
|
75
|
+
assert strip_disfluencies("escape them. Um. So on") == "escape them. So on"
|
|
76
|
+
assert strip_disfluencies("the scope of this? Um. I want") == "the scope of this? I want"
|
|
77
|
+
assert strip_disfluencies("Um. I want it") == "I want it"
|
|
78
|
+
|
|
79
|
+
def test_word_after_a_sentence_opening_filler_is_capitalized(self):
|
|
80
|
+
# Else "those. so I" reads as a false stop and cleanup makes it a comma.
|
|
81
|
+
assert strip_disfluencies("most of those. Um so I") == "most of those. So I"
|
|
82
|
+
assert strip_disfluencies("might be. Um uh yeah, and") == "might be. Yeah, and"
|
|
83
|
+
assert cleanup(strip_disfluencies("most of those. Um so I")) == "Most of those. So I"
|
|
84
|
+
assert strip_disfluencies("it was um uh good") == "it was good"
|
|
85
|
+
|
|
73
86
|
def test_uh_huh_removed_whole(self):
|
|
74
87
|
# Regression: ordering bug once stranded "-huh".
|
|
75
88
|
assert strip_disfluencies("uh-huh right") == "right"
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""Parakeet / sherpa-onnx: one recognizer, per-stream hotwords, chunking,
|
|
2
|
+
default selection, model download. sherpa-onnx is faked: no weights."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import io
|
|
7
|
+
import sys
|
|
8
|
+
import tarfile
|
|
9
|
+
import threading
|
|
10
|
+
import types
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from voiceio import config
|
|
16
|
+
from voiceio.config import ModelConfig
|
|
17
|
+
from voiceio.core.decoders import BiasMode, build_decoder, catalog, fetch, sherpa
|
|
18
|
+
from voiceio.core.decoders.base import Bias, DecodeError
|
|
19
|
+
from voiceio.core.decoders.sherpa import SherpaFeatures, SherpaTransducer, Tokenizer
|
|
20
|
+
|
|
21
|
+
FIXED = SherpaFeatures(installed=True, length_reward=True, pretokenized_hotwords=True)
|
|
22
|
+
TOKENS = "\n".join(f"{p} {i}" for i, p in enumerate(
|
|
23
|
+
["<unk>", "▁K", "al", "▁Kal", "sh", "hi", "shi", "▁", "K", "a", "l", "s", "h", "i",
|
|
24
|
+
"▁H", "▁Hy", "per", "p", "e", "r", "y", "H", "▁k", "k", "er"])) + "\n"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FakeSherpa:
|
|
28
|
+
"""Records recognizers built and the hotwords of every stream."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, text="hello world", block: threading.Event | None = None):
|
|
31
|
+
self.created: list[dict] = []
|
|
32
|
+
self.streams: list[dict] = []
|
|
33
|
+
self.text = text
|
|
34
|
+
self.block = block
|
|
35
|
+
self.running = 0
|
|
36
|
+
self.max_running = 0
|
|
37
|
+
fake = self
|
|
38
|
+
|
|
39
|
+
class Stream:
|
|
40
|
+
def __init__(self, hotwords):
|
|
41
|
+
self.rec = {"hotwords": hotwords, "samples": 0}
|
|
42
|
+
fake.streams.append(self.rec)
|
|
43
|
+
|
|
44
|
+
def accept_waveform(self, sr, a):
|
|
45
|
+
assert sr == 16000 and a.dtype == np.float32
|
|
46
|
+
self.rec["samples"] = len(a)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def result(self):
|
|
50
|
+
return types.SimpleNamespace(text=f" {fake.text} ")
|
|
51
|
+
|
|
52
|
+
class Rec:
|
|
53
|
+
def create_stream(self, hotwords=None):
|
|
54
|
+
return Stream(hotwords)
|
|
55
|
+
|
|
56
|
+
def decode_stream(self, s):
|
|
57
|
+
fake.running += 1
|
|
58
|
+
fake.max_running = max(fake.max_running, fake.running)
|
|
59
|
+
if fake.block is not None:
|
|
60
|
+
fake.block.wait(5)
|
|
61
|
+
fake.running -= 1
|
|
62
|
+
|
|
63
|
+
class OfflineRecognizer:
|
|
64
|
+
@staticmethod
|
|
65
|
+
def from_transducer(**kw):
|
|
66
|
+
fake.created.append(kw)
|
|
67
|
+
return Rec()
|
|
68
|
+
|
|
69
|
+
self.module = types.SimpleNamespace(OfflineRecognizer=OfflineRecognizer)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@pytest.fixture
|
|
73
|
+
def fake(monkeypatch):
|
|
74
|
+
f = FakeSherpa()
|
|
75
|
+
monkeypatch.setitem(sys.modules, "sherpa_onnx", f.module)
|
|
76
|
+
monkeypatch.setattr(sherpa, "features", lambda: FIXED)
|
|
77
|
+
return f
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _model_dir(tmp_path):
|
|
81
|
+
for name in ("encoder.int8.onnx", "decoder.int8.onnx", "joiner.int8.onnx"):
|
|
82
|
+
(tmp_path / name).write_text("x")
|
|
83
|
+
(tmp_path / "tokens.txt").write_text(TOKENS, encoding="utf-8")
|
|
84
|
+
return tmp_path
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _audio(secs: float) -> np.ndarray:
|
|
88
|
+
return np.full(int(secs * 16000), 0.1, dtype=np.float32)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ── tokenizer ────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
def test_tokenizer_merges_by_rank_like_sentencepiece_bpe(tmp_path):
|
|
94
|
+
tok = Tokenizer(_model_dir(tmp_path) / "tokens.txt")
|
|
95
|
+
assert tok.encode("Kalshi") == ["▁Kal", "shi"]
|
|
96
|
+
assert tok.encode("Hyper Kalshi") == ["▁Hy", "per", "▁Kal", "shi"]
|
|
97
|
+
assert tok.encode("Kalshi™") is None # a character with no piece
|
|
98
|
+
assert "<unk>" not in tok.bpe_vocab() and "▁Kal\t-3\n" in tok.bpe_vocab()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ── the decoder ──────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
def test_one_recognizer_and_per_stream_pretokenized_proper_nouns(fake, tmp_path):
|
|
104
|
+
dec = SherpaTransducer(_model_dir(tmp_path), label="parakeet-tdt-0.6b-v2")
|
|
105
|
+
assert dec.capabilities.bias == BiasMode.UNBOUNDED
|
|
106
|
+
assert dec.name == "sherpa:parakeet-tdt-0.6b-v2"
|
|
107
|
+
kw = fake.created[0]
|
|
108
|
+
assert kw["decoding_method"] == "modified_beam_search"
|
|
109
|
+
assert (kw["length_reward"], kw["max_active_paths"], kw["hotwords_score"]) == (0.5, 4, 2.0)
|
|
110
|
+
assert "hotwords_file" not in kw and kw["modeling_unit"] == "bpe"
|
|
111
|
+
assert (tmp_path / "bpe.vocab").exists()
|
|
112
|
+
|
|
113
|
+
out = dec.decode(_audio(2), final=True, bias=Bias(hotwords="Kalshi, kalshi, Hyper, a/b"))
|
|
114
|
+
assert out.text == "hello world"
|
|
115
|
+
# Lowercase (mined jargon) and unusable terms never become hotwords.
|
|
116
|
+
assert fake.streams[-1]["hotwords"] == "▁Kal shi/▁Hy per"
|
|
117
|
+
dec.decode(_audio(2), bias=Bias(hotwords="Hyper"))
|
|
118
|
+
dec.decode(_audio(2), bias=Bias())
|
|
119
|
+
assert [s["hotwords"] for s in fake.streams[1:]] == ["▁Hy per", None]
|
|
120
|
+
assert len(fake.created) == 1, "a new hotword set must never reload the model"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_sherpa_tokenization_fallback_passes_terms(fake, monkeypatch, tmp_path):
|
|
124
|
+
monkeypatch.setattr(sherpa, "features", lambda: SherpaFeatures(True, True, False))
|
|
125
|
+
dec = SherpaTransducer(_model_dir(tmp_path))
|
|
126
|
+
dec.decode(_audio(1), bias=Bias(hotwords="Kalshi, Hyper Kalshi"))
|
|
127
|
+
assert fake.streams[-1]["hotwords"] == "Kalshi/Hyper Kalshi"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_without_the_tdt_fix_decodes_greedy_and_unbiased(fake, monkeypatch, tmp_path):
|
|
131
|
+
monkeypatch.setattr(sherpa, "features", lambda: SherpaFeatures(True, False, False))
|
|
132
|
+
dec = build_decoder(ModelConfig(name=f"sherpa:{_model_dir(tmp_path)}"))
|
|
133
|
+
assert dec.capabilities.bias == BiasMode.NONE
|
|
134
|
+
assert dec.name.endswith("@greedy_search")
|
|
135
|
+
assert dec.decode(_audio(1), bias=Bias(hotwords="Kalshi")).text == "hello world"
|
|
136
|
+
kw = fake.created[-1]
|
|
137
|
+
assert kw["decoding_method"] == "greedy_search" and "length_reward" not in kw
|
|
138
|
+
assert fake.streams[-1]["hotwords"] is None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_long_audio_is_decoded_in_pieces_of_at_most_30s(fake, tmp_path):
|
|
142
|
+
dec = SherpaTransducer(_model_dir(tmp_path))
|
|
143
|
+
audio = _audio(70)
|
|
144
|
+
audio[int(25 * 16000):int(25.4 * 16000)] = 0.0 # a pause to cut at
|
|
145
|
+
out = dec.decode(audio, final=True)
|
|
146
|
+
sizes = [s["samples"] for s in fake.streams]
|
|
147
|
+
assert sum(sizes) == len(audio) and all(n <= 30 * 16000 for n in sizes)
|
|
148
|
+
assert len(sizes) == 3 and 24.5 * 16000 < sizes[0] <= 25.5 * 16000
|
|
149
|
+
assert out.text == "hello world hello world hello world"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_empty_audio_is_silence_not_an_error(fake, tmp_path):
|
|
153
|
+
dec = SherpaTransducer(_model_dir(tmp_path))
|
|
154
|
+
assert dec.decode(np.zeros(0, dtype=np.float32)).text == ""
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_decodes_run_concurrently_so_stop_never_queues(monkeypatch, tmp_path):
|
|
158
|
+
gate = threading.Event()
|
|
159
|
+
f = FakeSherpa(block=gate)
|
|
160
|
+
monkeypatch.setitem(sys.modules, "sherpa_onnx", f.module)
|
|
161
|
+
monkeypatch.setattr(sherpa, "features", lambda: FIXED)
|
|
162
|
+
dec = SherpaTransducer(_model_dir(tmp_path))
|
|
163
|
+
assert dec.draft is dec # lets stop decode past a pending freeze
|
|
164
|
+
threads = [threading.Thread(target=dec.decode, args=(_audio(1),)) for _ in range(2)]
|
|
165
|
+
for t in threads:
|
|
166
|
+
t.start()
|
|
167
|
+
for _ in range(200):
|
|
168
|
+
if f.running == 2:
|
|
169
|
+
break
|
|
170
|
+
threading.Event().wait(0.01)
|
|
171
|
+
assert f.max_running == 2
|
|
172
|
+
assert not dec.wait_idle(timeout=0.05)
|
|
173
|
+
gate.set()
|
|
174
|
+
for t in threads:
|
|
175
|
+
t.join(5)
|
|
176
|
+
assert dec.wait_idle(timeout=1)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_engine_errors_are_decode_errors(fake, tmp_path):
|
|
180
|
+
dec = SherpaTransducer(_model_dir(tmp_path))
|
|
181
|
+
|
|
182
|
+
def boom(s):
|
|
183
|
+
raise RuntimeError("ort")
|
|
184
|
+
dec._rec.decode_stream = boom
|
|
185
|
+
with pytest.raises(DecodeError):
|
|
186
|
+
dec.decode(_audio(1))
|
|
187
|
+
assert dec.wait_idle(timeout=1)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ── default selection ────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
def test_default_is_parakeet_only_when_sherpa_has_the_fix(monkeypatch):
|
|
193
|
+
assert catalog.default_model("en") == "small" # conftest: no sherpa
|
|
194
|
+
monkeypatch.setattr(sherpa, "features", lambda: SherpaFeatures(True, False, False))
|
|
195
|
+
assert catalog.default_model("en") == "small"
|
|
196
|
+
monkeypatch.setattr(sherpa, "features", lambda: SherpaFeatures(True, True, False))
|
|
197
|
+
assert catalog.default_model("en") == catalog.PARAKEET
|
|
198
|
+
assert catalog.default_model("es") == "small" # Parakeet v2 is English only
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_config_resolves_unset_or_auto_name_but_an_explicit_choice_wins(monkeypatch, tmp_path):
|
|
202
|
+
monkeypatch.setattr(sherpa, "features", lambda: FIXED)
|
|
203
|
+
path = tmp_path / "c.toml"
|
|
204
|
+
path.write_text('[model]\nlanguage = "en"\n')
|
|
205
|
+
assert config.load(path).model.name == catalog.PARAKEET
|
|
206
|
+
path.write_text('[model]\nname = "auto"\n')
|
|
207
|
+
assert config.load(path).model.name == catalog.PARAKEET
|
|
208
|
+
for explicit in ("small", "/home/u/.local/share/voiceio/learn/models/v0001-small-x/ct2"):
|
|
209
|
+
path.write_text(f'[model]\nname = "{explicit}"\n')
|
|
210
|
+
assert config.load(path).model.name == explicit
|
|
211
|
+
monkeypatch.setattr(sherpa, "features", lambda: SherpaFeatures())
|
|
212
|
+
path.write_text('[model]\nlanguage = "en"\n')
|
|
213
|
+
assert config.load(path).model.name == "small"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_runtime_and_whisper_detection():
|
|
217
|
+
assert catalog.runtime_of(catalog.PARAKEET) == "sherpa-onnx"
|
|
218
|
+
assert catalog.runtime_of("sherpa:/models/x") == "sherpa-onnx"
|
|
219
|
+
assert catalog.is_whisper("small") and catalog.is_whisper("/learn/models/v1/ct2")
|
|
220
|
+
assert not catalog.is_whisper(catalog.PARAKEET)
|
|
221
|
+
m = catalog.info(catalog.PARAKEET)
|
|
222
|
+
assert m.url.startswith("https://github.com/k2-fsa/sherpa-onnx/releases/")
|
|
223
|
+
assert len(m.sha256) == 64 and "huggingface" not in m.url
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_catalog_model_is_fetched_on_first_use(fake, monkeypatch, tmp_path):
|
|
227
|
+
got = []
|
|
228
|
+
(tmp_path / "m").mkdir()
|
|
229
|
+
model = _model_dir(tmp_path / "m")
|
|
230
|
+
monkeypatch.setattr(fetch, "download", lambda name, **k: got.append(name) or model)
|
|
231
|
+
dec = build_decoder(ModelConfig(name=catalog.PARAKEET))
|
|
232
|
+
assert got == [catalog.PARAKEET] and dec.name == f"sherpa:{catalog.PARAKEET}"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_no_download_when_sherpa_is_missing(monkeypatch):
|
|
236
|
+
monkeypatch.setattr(fetch, "download", lambda *a, **k: pytest.fail("downloaded"))
|
|
237
|
+
with pytest.raises(DecodeError, match="sherpa-onnx not installed"):
|
|
238
|
+
build_decoder(ModelConfig(name=catalog.PARAKEET))
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ── download ─────────────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
def _tarball() -> bytes:
|
|
244
|
+
buf = io.BytesIO()
|
|
245
|
+
with tarfile.open(fileobj=buf, mode="w:bz2") as tar:
|
|
246
|
+
for name, data in (("encoder.int8.onnx", b"e"), ("decoder.int8.onnx", b"d"),
|
|
247
|
+
("joiner.int8.onnx", b"j"), ("tokens.txt", TOKENS.encode()),
|
|
248
|
+
("test_wavs/0.wav", b"w")):
|
|
249
|
+
info = tarfile.TarInfo(f"sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8/{name}")
|
|
250
|
+
info.size = len(data)
|
|
251
|
+
tar.addfile(info, io.BytesIO(data))
|
|
252
|
+
return buf.getvalue()
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _serve(monkeypatch, body: bytes):
|
|
256
|
+
class Resp(io.BytesIO):
|
|
257
|
+
headers = {"Content-Length": str(len(body))}
|
|
258
|
+
|
|
259
|
+
def __enter__(self):
|
|
260
|
+
return self
|
|
261
|
+
|
|
262
|
+
def __exit__(self, *a):
|
|
263
|
+
return False
|
|
264
|
+
|
|
265
|
+
monkeypatch.setattr("urllib.request.urlopen", lambda req, timeout=0: Resp(body))
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _entry(monkeypatch, body: bytes):
|
|
269
|
+
m = catalog.ModelInfo("pk-test", "sherpa-onnx", "1", "x", BiasMode.UNBOUNDED,
|
|
270
|
+
url="https://github.com/x.tar.bz2",
|
|
271
|
+
sha256=hashlib.sha256(body).hexdigest(), size_mb=1)
|
|
272
|
+
monkeypatch.setattr(fetch, "info", lambda name: m if name == "pk-test" else None)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def test_download_verifies_and_unpacks_without_sample_wavs(monkeypatch, tmp_path):
|
|
276
|
+
monkeypatch.setattr(config, "MODELS_DIR", tmp_path)
|
|
277
|
+
body = _tarball()
|
|
278
|
+
_entry(monkeypatch, body)
|
|
279
|
+
_serve(monkeypatch, body)
|
|
280
|
+
seen = []
|
|
281
|
+
path = fetch.download("pk-test", progress=lambda d, t: seen.append((d, t)))
|
|
282
|
+
assert path == tmp_path / "pk-test" and fetch.is_downloaded("pk-test")
|
|
283
|
+
assert not (path / "test_wavs").exists()
|
|
284
|
+
assert seen[-1] == (len(body), len(body))
|
|
285
|
+
assert sorted(p.name for p in tmp_path.iterdir()) == ["pk-test"] # no leftovers
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_download_with_a_bad_checksum_leaves_nothing(monkeypatch, tmp_path):
|
|
289
|
+
monkeypatch.setattr(config, "MODELS_DIR", tmp_path)
|
|
290
|
+
body = _tarball()
|
|
291
|
+
_entry(monkeypatch, body)
|
|
292
|
+
_serve(monkeypatch, body[:-10] + b"0123456789")
|
|
293
|
+
with pytest.raises(OSError, match="checksum"):
|
|
294
|
+
fetch.download("pk-test")
|
|
295
|
+
assert list(tmp_path.iterdir()) == []
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def test_patched_wheel_is_picked_for_this_python_and_cpu():
|
|
299
|
+
sums = ("aa sherpa_onnx-1.13.8-cp312-cp312-linux_x86_64.whl\n"
|
|
300
|
+
"bb sherpa_onnx-1.13.8-cp313-cp313-manylinux_2_28_x86_64.whl\n"
|
|
301
|
+
"cc sherpa_onnx-1.13.8-cp313-cp313-linux_aarch64.whl\n")
|
|
302
|
+
assert fetch.patched_sherpa_wheel(sums, (3, 13), "x86_64") == (
|
|
303
|
+
"sherpa_onnx-1.13.8-cp313-cp313-manylinux_2_28_x86_64.whl", "bb")
|
|
304
|
+
assert fetch.patched_sherpa_wheel(sums, (3, 11), "x86_64") is None
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# ── learn / doctor / eval ────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
def test_learn_says_fine_tunes_are_unused_while_parakeet_decodes(monkeypatch):
|
|
310
|
+
from voiceio.cli import learn
|
|
311
|
+
monkeypatch.setattr(learn, "_cfg", lambda: types.SimpleNamespace(
|
|
312
|
+
model=ModelConfig(name=catalog.PARAKEET)))
|
|
313
|
+
assert "isn't used" in learn._whisper_unused()
|
|
314
|
+
monkeypatch.setattr(learn, "_cfg", lambda: types.SimpleNamespace(
|
|
315
|
+
model=ModelConfig(name="small")))
|
|
316
|
+
assert learn._whisper_unused() == ""
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def test_maintain_labels_but_does_not_train_for_an_unused_whisper(monkeypatch, capsys):
|
|
320
|
+
import argparse
|
|
321
|
+
|
|
322
|
+
from voiceio.cli import learn
|
|
323
|
+
from voiceio.config import LearnConfig
|
|
324
|
+
monkeypatch.setattr(learn, "_cfg", lambda: types.SimpleNamespace(
|
|
325
|
+
model=ModelConfig(name=catalog.PARAKEET)))
|
|
326
|
+
labeled = []
|
|
327
|
+
monkeypatch.setattr(learn, "cmd_label", lambda a: labeled.append(a))
|
|
328
|
+
monkeypatch.setattr(learn, "_announce_learning", lambda n: None)
|
|
329
|
+
monkeypatch.setattr(learn, "_train", lambda *a, **k: pytest.fail("trained"))
|
|
330
|
+
learn._maintain(argparse.Namespace(), LearnConfig())
|
|
331
|
+
assert labeled and "not retraining" in capsys.readouterr().out
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_doctor_reports_the_decoder(monkeypatch):
|
|
335
|
+
from voiceio.health import _decoder_status, _learning_status
|
|
336
|
+
cfg = config.Config()
|
|
337
|
+
cfg.model.name = catalog.PARAKEET
|
|
338
|
+
assert "not installed" in _decoder_status(cfg)["Decoder"]
|
|
339
|
+
monkeypatch.setattr(sherpa, "features", lambda: SherpaFeatures(True, False, False))
|
|
340
|
+
assert "NO vocabulary biasing" in _decoder_status(cfg)["Decoder"]
|
|
341
|
+
monkeypatch.setattr(sherpa, "features", lambda: FIXED)
|
|
342
|
+
assert "pre-tokenized" in _decoder_status(cfg)["Decoder"]
|
|
343
|
+
assert "aren't used" in _learning_status(cfg)["Voice model"]
|
|
344
|
+
cfg.model.name = "small"
|
|
345
|
+
assert catalog.PARAKEET in _decoder_status(cfg)["Decoder"]
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def test_eval_results_are_found_for_sherpa_models(monkeypatch, tmp_path):
|
|
349
|
+
import json
|
|
350
|
+
|
|
351
|
+
from voiceio.learn import evaluate, metrics
|
|
352
|
+
runs = tmp_path / "runs"
|
|
353
|
+
runs.mkdir()
|
|
354
|
+
(runs / "eval-1.json").write_text(json.dumps({
|
|
355
|
+
"decoder": f"sherpa:{catalog.PARAKEET}", "dataset": "v1", "mode": "offline",
|
|
356
|
+
"metrics": metrics.METRICS_VERSION, "wer": 0.07}))
|
|
357
|
+
monkeypatch.setattr(evaluate, "learn_dir", lambda: tmp_path)
|
|
358
|
+
assert evaluate.latest(catalog.PARAKEET, "v1", beam=2)["wer"] == 0.07
|
|
359
|
+
assert evaluate.latest("small", "v1") is None
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def test_wizard_offers_parakeet_first_only_where_it_can_bias(monkeypatch):
|
|
363
|
+
from voiceio.setup import speech
|
|
364
|
+
assert speech.model_choices("en") is speech.MODELS
|
|
365
|
+
monkeypatch.setattr(sherpa, "features", lambda: FIXED)
|
|
366
|
+
choices = speech.model_choices("en")
|
|
367
|
+
assert choices[0][0] == catalog.PARAKEET and speech.default_choice(choices) == 0
|
|
368
|
+
assert not any("recommended" in c[2] for c in choices[1:])
|
|
369
|
+
assert speech.model_choices("es") is speech.MODELS
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.12.0"
|
|
@@ -159,6 +159,17 @@ def _cfg():
|
|
|
159
159
|
return config.load()
|
|
160
160
|
|
|
161
161
|
|
|
162
|
+
def _whisper_unused() -> str:
|
|
163
|
+
"""Why a Whisper fine-tune does nothing right now, or "": `learn` only
|
|
164
|
+
trains Whisper, and dictation may decode with something else."""
|
|
165
|
+
from voiceio.core.decoders.catalog import is_whisper
|
|
166
|
+
active = _cfg().model.name
|
|
167
|
+
if is_whisper(active):
|
|
168
|
+
return ""
|
|
169
|
+
return (f"dictation decodes with {active}, not Whisper: a fine-tune trains Whisper and "
|
|
170
|
+
f"isn't used unless you switch back (voiceio learn promote RUN)")
|
|
171
|
+
|
|
172
|
+
|
|
162
173
|
def _vocabulary() -> list[str]:
|
|
163
174
|
from voiceio.vocabulary import VocabularyLoader
|
|
164
175
|
return VocabularyLoader(_cfg().model).get_all()
|
|
@@ -196,6 +207,8 @@ def cmd_status(args: argparse.Namespace) -> None:
|
|
|
196
207
|
print(f"models {', '.join(runs)}")
|
|
197
208
|
active = _cfg().model.name
|
|
198
209
|
print(f"active {active}")
|
|
210
|
+
if unused := _whisper_unused():
|
|
211
|
+
print(f"note {unused}")
|
|
199
212
|
print(f"nightly {_schedule_state()}")
|
|
200
213
|
promotable = _print_scores(ds, active, runs) if ds else None
|
|
201
214
|
if promotable:
|
|
@@ -409,6 +422,8 @@ def _eval_pipeline() -> None:
|
|
|
409
422
|
|
|
410
423
|
def cmd_train(args: argparse.Namespace) -> None:
|
|
411
424
|
_yield_cpu()
|
|
425
|
+
if unused := _whisper_unused():
|
|
426
|
+
print(f"note: {unused}")
|
|
412
427
|
ds = store.latest_dataset()
|
|
413
428
|
if ds is None:
|
|
414
429
|
raise SystemExit("no dataset — run `voiceio learn build` first")
|
|
@@ -535,6 +550,11 @@ def _maintain(args: argparse.Namespace, learn_cfg) -> None:
|
|
|
535
550
|
if not os.environ.get(quiet.RESUMED_ENV):
|
|
536
551
|
_announce_learning(learn_cfg.min_new)
|
|
537
552
|
cmd_label(argparse.Namespace(teacher=learn_cfg.teacher or None, limit=None, relabel=False))
|
|
553
|
+
if unused := _whisper_unused():
|
|
554
|
+
# Labels still pay off (evals, a later switch back); nightly training
|
|
555
|
+
# of a model dictation doesn't use would only burn the CPU.
|
|
556
|
+
print(f"labeled; not retraining — {unused}. `voiceio learn train` trains anyway.")
|
|
557
|
+
return
|
|
538
558
|
if missing := train.missing_train_deps():
|
|
539
559
|
# Labeling alone is worth it; retraining waits for the extra. Exit 0:
|
|
540
560
|
# a nightly timer must not fail every night over an optional install.
|