python-voiceio 1.11.0__tar.gz → 1.13.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.13.0}/PKG-INFO +4 -4
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/README.md +3 -3
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/pyproject.toml +1 -1
- {python_voiceio-1.11.0 → python_voiceio-1.13.0/python_voiceio.egg-info}/PKG-INFO +4 -4
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/python_voiceio.egg-info/SOURCES.txt +3 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_config.py +6 -1
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_decoders.py +0 -56
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_postprocess.py +13 -0
- python_voiceio-1.13.0/tests/test_sherpa.py +374 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_tts.py +76 -0
- python_voiceio-1.13.0/voiceio/__init__.py +1 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/cli/learn.py +20 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/cli/models.py +88 -8
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/config.py +18 -7
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/core/decoders/__init__.py +32 -11
- python_voiceio-1.13.0/voiceio/core/decoders/catalog.py +119 -0
- python_voiceio-1.13.0/voiceio/core/decoders/fetch.py +208 -0
- python_voiceio-1.13.0/voiceio/core/decoders/sherpa.py +304 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/health.py +26 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/learn/evaluate.py +8 -3
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/postprocess.py +20 -4
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/server/runtime.py +1 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/service.py +1 -1
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/setup/noninteractive.py +5 -4
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/setup/speech.py +42 -1
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/setup/steps.py +11 -7
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/tts/chain.py +4 -1
- python_voiceio-1.13.0/voiceio/tts/supertonic_engine.py +91 -0
- python_voiceio-1.11.0/voiceio/__init__.py +0 -1
- python_voiceio-1.11.0/voiceio/core/decoders/catalog.py +0 -59
- python_voiceio-1.11.0/voiceio/core/decoders/sherpa.py +0 -108
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/LICENSE +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/python_voiceio.egg-info/requires.txt +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/setup.cfg +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_app_wiring.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_audio_quality.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_audio_source.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_backend_probes.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_cancel.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_cli.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_clipboard_read.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_clipboard_typer.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_commands.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_concurrency_lockdown.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_context_terms.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_corrections.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_dead_mic.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_engine.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_evdev_uaccess.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_fallback.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_fcitx_bridge.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_feedback_stream.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_focus.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_health.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_hints.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_history.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_hotkey_modes.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_ibus_engine_limits.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_ibus_engine_listener.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_ibus_pending.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_ibus_pids.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_ibus_ping.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_ibus_session.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_ibus_textlimit.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_ibus_typer.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_idle_audio.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_learn_clean.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_learn_import.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_learn_language.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_learn_metrics.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_learn_pipeline.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_learn_quiet.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_learn_resume.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_learn_review.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_learn_targets.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_llm.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_llm_api.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_mic_gain.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_modifier_guard.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_numbers.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_paste_last.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_pipeline.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_platform.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_postcorrect.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_prebuffer.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_presence.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_prompt.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_quiet_audio.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_report.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_retention.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_robustness.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_security_hardening.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_server.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_service.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_setup.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_streaming.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_suggest.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_tokens.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_transcriber.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_typer_manager.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_vad.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_vocabulary.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_wordfreq.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/tests/test_worker_decode.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/__main__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/app.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/audio_source.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/backends.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/cli/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/cli/configcmd.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/cli/corrections.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/cli/doctor.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/cli/history.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/cli/servicecmd.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/cli/uninstall.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/cli/vocab.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/clipboard_read.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/commands.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/consent.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/context_terms.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/core/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/core/bias.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/core/decoders/base.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/core/decoders/faster_whisper/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/core/decoders/lanes.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/core/decoders/whispercpp.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/core/engine.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/core/pipeline.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/corrections.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/data/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/demo.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/display.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/feedback.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/focus.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/hints.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/history.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/hotkeys/socket_backend.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/ibus/engine.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/ibus/install.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/ibus/pending.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/ibus/session.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/ibus/textlimit.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/idle_audio.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/learn/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/learn/augment.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/learn/clean.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/learn/importer.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/learn/metrics.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/learn/quiet.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/learn/review.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/learn/store.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/learn/teacher.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/learn/train.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/llm.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/llm_api.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/mic_gain.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/models/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/numbers.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/pidlock.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/platform.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/postcorrect.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/power.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/presence.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/privacy.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/prompt.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/quiet_audio.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/recorder.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/report.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/retention.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/server/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/server/__main__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/server/app.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/server/config.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/setup/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/setup/configfile.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/setup/extras.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/setup/hotkey.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/setup/system.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/setup/ui.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/sounds/alert.wav +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/streaming.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/suggest.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/tokens.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/tray/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/tray/_indicator.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/tray/_pystray.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/tts/base.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/tts/player.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/typers/base.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/typers/chain.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/typers/collecting.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/typers/fcitx_bridge.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/typers/ibus.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/typers/manager.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/typers/modifier_guard.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/typers/paste.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/typers/ydotool.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/ui.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/vad.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/vocab_stats.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/vocabulary.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.0}/voiceio/watchdog.py +0 -0
- {python_voiceio-1.11.0 → python_voiceio-1.13.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.13.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 (text lands ~0.7 s after you stop talking instead of ~4.4 s, 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 (text lands ~0.7 s after you stop talking instead of ~4.4 s, 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.13.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 (text lands ~0.7 s after you stop talking instead of ~4.4 s, 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
|
|
@@ -206,6 +208,7 @@ voiceio/tts/edge_engine.py
|
|
|
206
208
|
voiceio/tts/espeak.py
|
|
207
209
|
voiceio/tts/piper_engine.py
|
|
208
210
|
voiceio/tts/player.py
|
|
211
|
+
voiceio/tts/supertonic_engine.py
|
|
209
212
|
voiceio/typers/__init__.py
|
|
210
213
|
voiceio/typers/base.py
|
|
211
214
|
voiceio/typers/chain.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,374 @@
|
|
|
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") == catalog.PARAKEET_V3
|
|
199
|
+
assert catalog.default_model("ja") == "small" # outside Parakeet v3's languages
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_config_resolves_unset_or_auto_name_but_an_explicit_choice_wins(monkeypatch, tmp_path):
|
|
203
|
+
monkeypatch.setattr(sherpa, "features", lambda: FIXED)
|
|
204
|
+
path = tmp_path / "c.toml"
|
|
205
|
+
path.write_text('[model]\nlanguage = "en"\n')
|
|
206
|
+
assert config.load(path).model.name == catalog.PARAKEET
|
|
207
|
+
path.write_text('[model]\nname = "auto"\n')
|
|
208
|
+
assert config.load(path).model.name == catalog.PARAKEET
|
|
209
|
+
for explicit in ("small", "/home/u/.local/share/voiceio/learn/models/v0001-small-x/ct2"):
|
|
210
|
+
path.write_text(f'[model]\nname = "{explicit}"\n')
|
|
211
|
+
assert config.load(path).model.name == explicit
|
|
212
|
+
monkeypatch.setattr(sherpa, "features", lambda: SherpaFeatures())
|
|
213
|
+
path.write_text('[model]\nlanguage = "en"\n')
|
|
214
|
+
assert config.load(path).model.name == "small"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_runtime_and_whisper_detection():
|
|
218
|
+
assert catalog.runtime_of(catalog.PARAKEET) == "sherpa-onnx"
|
|
219
|
+
assert catalog.runtime_of("sherpa:/models/x") == "sherpa-onnx"
|
|
220
|
+
assert catalog.is_whisper("small") and catalog.is_whisper("/learn/models/v1/ct2")
|
|
221
|
+
assert not catalog.is_whisper(catalog.PARAKEET)
|
|
222
|
+
m = catalog.info(catalog.PARAKEET)
|
|
223
|
+
assert m.url.startswith("https://github.com/k2-fsa/sherpa-onnx/releases/")
|
|
224
|
+
assert len(m.sha256) == 64 and "huggingface" not in m.url
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_catalog_model_is_fetched_on_first_use(fake, monkeypatch, tmp_path):
|
|
228
|
+
got = []
|
|
229
|
+
(tmp_path / "m").mkdir()
|
|
230
|
+
model = _model_dir(tmp_path / "m")
|
|
231
|
+
monkeypatch.setattr(fetch, "download", lambda name, **k: got.append(name) or model)
|
|
232
|
+
dec = build_decoder(ModelConfig(name=catalog.PARAKEET))
|
|
233
|
+
assert got == [catalog.PARAKEET] and dec.name == f"sherpa:{catalog.PARAKEET}"
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_no_download_when_sherpa_is_missing(monkeypatch):
|
|
237
|
+
monkeypatch.setattr(fetch, "download", lambda *a, **k: pytest.fail("downloaded"))
|
|
238
|
+
with pytest.raises(DecodeError, match="sherpa-onnx not installed"):
|
|
239
|
+
build_decoder(ModelConfig(name=catalog.PARAKEET))
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ── download ─────────────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
def _tarball() -> bytes:
|
|
245
|
+
buf = io.BytesIO()
|
|
246
|
+
with tarfile.open(fileobj=buf, mode="w:bz2") as tar:
|
|
247
|
+
for name, data in (("encoder.int8.onnx", b"e"), ("decoder.int8.onnx", b"d"),
|
|
248
|
+
("joiner.int8.onnx", b"j"), ("tokens.txt", TOKENS.encode()),
|
|
249
|
+
("test_wavs/0.wav", b"w")):
|
|
250
|
+
info = tarfile.TarInfo(f"sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8/{name}")
|
|
251
|
+
info.size = len(data)
|
|
252
|
+
tar.addfile(info, io.BytesIO(data))
|
|
253
|
+
return buf.getvalue()
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _serve(monkeypatch, body: bytes):
|
|
257
|
+
class Resp(io.BytesIO):
|
|
258
|
+
headers = {"Content-Length": str(len(body))}
|
|
259
|
+
|
|
260
|
+
def __enter__(self):
|
|
261
|
+
return self
|
|
262
|
+
|
|
263
|
+
def __exit__(self, *a):
|
|
264
|
+
return False
|
|
265
|
+
|
|
266
|
+
monkeypatch.setattr("urllib.request.urlopen", lambda req, timeout=0: Resp(body))
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _entry(monkeypatch, body: bytes):
|
|
270
|
+
m = catalog.ModelInfo("pk-test", "sherpa-onnx", "1", "x", BiasMode.UNBOUNDED,
|
|
271
|
+
url="https://github.com/x.tar.bz2",
|
|
272
|
+
sha256=hashlib.sha256(body).hexdigest(), size_mb=1)
|
|
273
|
+
monkeypatch.setattr(fetch, "info", lambda name: m if name == "pk-test" else None)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_download_verifies_and_unpacks_without_sample_wavs(monkeypatch, tmp_path):
|
|
277
|
+
monkeypatch.setattr(config, "MODELS_DIR", tmp_path)
|
|
278
|
+
body = _tarball()
|
|
279
|
+
_entry(monkeypatch, body)
|
|
280
|
+
_serve(monkeypatch, body)
|
|
281
|
+
seen = []
|
|
282
|
+
path = fetch.download("pk-test", progress=lambda d, t: seen.append((d, t)))
|
|
283
|
+
assert path == tmp_path / "pk-test" and fetch.is_downloaded("pk-test")
|
|
284
|
+
assert not (path / "test_wavs").exists()
|
|
285
|
+
assert seen[-1] == (len(body), len(body))
|
|
286
|
+
assert sorted(p.name for p in tmp_path.iterdir()) == ["pk-test"] # no leftovers
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def test_download_with_a_bad_checksum_leaves_nothing(monkeypatch, tmp_path):
|
|
290
|
+
monkeypatch.setattr(config, "MODELS_DIR", tmp_path)
|
|
291
|
+
body = _tarball()
|
|
292
|
+
_entry(monkeypatch, body)
|
|
293
|
+
_serve(monkeypatch, body[:-10] + b"0123456789")
|
|
294
|
+
with pytest.raises(OSError, match="checksum"):
|
|
295
|
+
fetch.download("pk-test")
|
|
296
|
+
assert list(tmp_path.iterdir()) == []
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def test_patched_wheel_is_picked_for_this_python_and_cpu():
|
|
300
|
+
sums = ("aa sherpa_onnx-1.13.8-cp312-cp312-linux_x86_64.whl\n"
|
|
301
|
+
"bb sherpa_onnx-1.13.8-cp313-cp313-manylinux_2_28_x86_64.whl\n"
|
|
302
|
+
"cc sherpa_onnx-1.13.8-cp313-cp313-linux_aarch64.whl\n")
|
|
303
|
+
assert fetch.patched_sherpa_wheel(sums, (3, 13), "x86_64") == (
|
|
304
|
+
"sherpa_onnx-1.13.8-cp313-cp313-manylinux_2_28_x86_64.whl", "bb")
|
|
305
|
+
assert fetch.patched_sherpa_wheel(sums, (3, 11), "x86_64") is None
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# ── learn / doctor / eval ────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
def test_learn_says_fine_tunes_are_unused_while_parakeet_decodes(monkeypatch):
|
|
311
|
+
from voiceio.cli import learn
|
|
312
|
+
monkeypatch.setattr(learn, "_cfg", lambda: types.SimpleNamespace(
|
|
313
|
+
model=ModelConfig(name=catalog.PARAKEET)))
|
|
314
|
+
assert "isn't used" in learn._whisper_unused()
|
|
315
|
+
monkeypatch.setattr(learn, "_cfg", lambda: types.SimpleNamespace(
|
|
316
|
+
model=ModelConfig(name="small")))
|
|
317
|
+
assert learn._whisper_unused() == ""
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def test_maintain_labels_but_does_not_train_for_an_unused_whisper(monkeypatch, capsys):
|
|
321
|
+
import argparse
|
|
322
|
+
|
|
323
|
+
from voiceio.cli import learn
|
|
324
|
+
from voiceio.config import LearnConfig
|
|
325
|
+
monkeypatch.setattr(learn, "_cfg", lambda: types.SimpleNamespace(
|
|
326
|
+
model=ModelConfig(name=catalog.PARAKEET)))
|
|
327
|
+
labeled = []
|
|
328
|
+
monkeypatch.setattr(learn, "cmd_label", lambda a: labeled.append(a))
|
|
329
|
+
monkeypatch.setattr(learn, "_announce_learning", lambda n: None)
|
|
330
|
+
monkeypatch.setattr(learn, "_train", lambda *a, **k: pytest.fail("trained"))
|
|
331
|
+
learn._maintain(argparse.Namespace(), LearnConfig())
|
|
332
|
+
assert labeled and "not retraining" in capsys.readouterr().out
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def test_doctor_reports_the_decoder(monkeypatch):
|
|
336
|
+
from voiceio.health import _decoder_status, _learning_status
|
|
337
|
+
cfg = config.Config()
|
|
338
|
+
cfg.model.name = catalog.PARAKEET
|
|
339
|
+
assert "not installed" in _decoder_status(cfg)["Decoder"]
|
|
340
|
+
monkeypatch.setattr(sherpa, "features", lambda: SherpaFeatures(True, False, False))
|
|
341
|
+
assert "NO vocabulary biasing" in _decoder_status(cfg)["Decoder"]
|
|
342
|
+
monkeypatch.setattr(sherpa, "features", lambda: FIXED)
|
|
343
|
+
assert "pre-tokenized" in _decoder_status(cfg)["Decoder"]
|
|
344
|
+
assert "aren't used" in _learning_status(cfg)["Voice model"]
|
|
345
|
+
cfg.model.name = "small"
|
|
346
|
+
assert catalog.PARAKEET in _decoder_status(cfg)["Decoder"]
|
|
347
|
+
cfg.model.language = "es"
|
|
348
|
+
assert catalog.PARAKEET_V3 in _decoder_status(cfg)["Decoder"]
|
|
349
|
+
cfg.model.language = "ja"
|
|
350
|
+
assert _decoder_status(cfg) == {}
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def test_eval_results_are_found_for_sherpa_models(monkeypatch, tmp_path):
|
|
354
|
+
import json
|
|
355
|
+
|
|
356
|
+
from voiceio.learn import evaluate, metrics
|
|
357
|
+
runs = tmp_path / "runs"
|
|
358
|
+
runs.mkdir()
|
|
359
|
+
(runs / "eval-1.json").write_text(json.dumps({
|
|
360
|
+
"decoder": f"sherpa:{catalog.PARAKEET}", "dataset": "v1", "mode": "offline",
|
|
361
|
+
"metrics": metrics.METRICS_VERSION, "wer": 0.07}))
|
|
362
|
+
monkeypatch.setattr(evaluate, "learn_dir", lambda: tmp_path)
|
|
363
|
+
assert evaluate.latest(catalog.PARAKEET, "v1", beam=2)["wer"] == 0.07
|
|
364
|
+
assert evaluate.latest("small", "v1") is None
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def test_wizard_offers_parakeet_first_only_where_it_can_bias(monkeypatch):
|
|
368
|
+
from voiceio.setup import speech
|
|
369
|
+
assert speech.model_choices("en") is speech.MODELS
|
|
370
|
+
monkeypatch.setattr(sherpa, "features", lambda: FIXED)
|
|
371
|
+
choices = speech.model_choices("en")
|
|
372
|
+
assert choices[0][0] == catalog.PARAKEET and speech.default_choice(choices) == 0
|
|
373
|
+
assert not any("recommended" in c[2] for c in choices[1:])
|
|
374
|
+
assert speech.model_choices("es") is speech.MODELS
|