python-voiceio 0.9.5__tar.gz → 0.9.6__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-0.9.5/python_voiceio.egg-info → python_voiceio-0.9.6}/PKG-INFO +1 -1
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/pyproject.toml +1 -1
- {python_voiceio-0.9.5 → python_voiceio-0.9.6/python_voiceio.egg-info}/PKG-INFO +1 -1
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_postcorrect.py +53 -1
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_postprocess.py +47 -1
- python_voiceio-0.9.6/voiceio/__init__.py +1 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/app.py +3 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/config.py +6 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/postcorrect.py +70 -19
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/postprocess.py +43 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/streaming.py +3 -0
- python_voiceio-0.9.5/voiceio/__init__.py +0 -1
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/LICENSE +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/README.md +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/python_voiceio.egg-info/SOURCES.txt +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/python_voiceio.egg-info/requires.txt +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/setup.cfg +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_adjudicate.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_app_wiring.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_audio_quality.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_audit.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_autocorrect.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_autocorrect_state.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_backend_probes.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_cli.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_clipboard_read.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_commands.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_concurrency_lockdown.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_config.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_correct_batch.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_corrections.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_evaluate.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_fallback.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_health.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_hints.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_history.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_ibus_pending.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_ibus_ping.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_ibus_typer.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_llm.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_llm_api.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_numbers.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_platform.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_prebuffer.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_prompt.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_retention.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_robustness.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_security_hardening.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_snapshots.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_streaming.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_tokens.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_transcriber.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_tts.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_vad.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_vocabulary.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_wizard.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/tests/test_wordfreq.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/__main__.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/audit.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/autocorrect.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/autocorrect_state.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/backends.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/cli.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/clipboard_read.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/commands.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/consent.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/corrections.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/demo.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/evaluate.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/feedback.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/health.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/hints.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/history.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/hotkeys/socket_backend.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/ibus/engine.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/ibus/pending.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/llm.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/llm_api.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/models/__init__.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/numbers.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/pidlock.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/platform.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/prompt.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/recorder.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/retention.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/service.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/snapshots.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/tokens.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/transcriber.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/tray/__init__.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/tray/_indicator.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/tray/_pystray.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/tts/base.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/tts/chain.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/tts/player.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/typers/base.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/typers/chain.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/typers/ibus.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/typers/ydotool.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/vad.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/vocab_stats.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/vocabulary.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/wizard.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/wordfreq.py +0 -0
- {python_voiceio-0.9.5 → python_voiceio-0.9.6}/voiceio/worker.py +0 -0
|
@@ -7,11 +7,13 @@ from voiceio.config import Config, PostCorrectConfig
|
|
|
7
7
|
from voiceio.postcorrect import PostCorrector
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def _cfg(*, enabled=True, api_key="test-key", min_words=4, model=""
|
|
10
|
+
def _cfg(*, enabled=True, api_key="test-key", min_words=4, model="",
|
|
11
|
+
remove_disfluencies=False) -> Config:
|
|
11
12
|
cfg = Config()
|
|
12
13
|
cfg.postcorrect = PostCorrectConfig(
|
|
13
14
|
enabled=enabled, min_words=min_words, model=model,
|
|
14
15
|
)
|
|
16
|
+
cfg.output.remove_disfluencies = remove_disfluencies
|
|
15
17
|
cfg.autocorrect.api_key = api_key
|
|
16
18
|
return cfg
|
|
17
19
|
|
|
@@ -79,6 +81,56 @@ def test_word_count_guard_rejects_length_change():
|
|
|
79
81
|
assert pc.correct(original) == original
|
|
80
82
|
|
|
81
83
|
|
|
84
|
+
# ── disfluency mode: delete-only, never change meaning ───────────────────
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_disfluency_mode_allows_deletion():
|
|
88
|
+
"""Removing filler shrinks the text well past the 20% fix-mode ceiling —
|
|
89
|
+
the disfluency guard must allow it (deletions only)."""
|
|
90
|
+
pc = PostCorrector(_cfg(remove_disfluencies=True))
|
|
91
|
+
original = "so um we should like ship the thing today"
|
|
92
|
+
cleaned = "so we should ship the thing today" # dropped 'um' and 'like'
|
|
93
|
+
with patch("voiceio.llm_api.chat", return_value=cleaned):
|
|
94
|
+
assert pc.correct(original) == cleaned
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_disfluency_mode_rejects_insertion():
|
|
98
|
+
"""Adding any word could alter meaning — reject, keep the original."""
|
|
99
|
+
pc = PostCorrector(_cfg(remove_disfluencies=True))
|
|
100
|
+
original = "we should ship the thing today"
|
|
101
|
+
added = "we should ship the thing today and also test everything first"
|
|
102
|
+
with patch("voiceio.llm_api.chat", return_value=added):
|
|
103
|
+
assert pc.correct(original) == original
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_disfluency_mode_rejects_overdeletion():
|
|
107
|
+
"""Deleting more than the cap means real content was stripped — reject."""
|
|
108
|
+
pc = PostCorrector(_cfg(remove_disfluencies=True))
|
|
109
|
+
original = "please run the full test suite before you deploy tonight okay"
|
|
110
|
+
gutted = "please run test deploy" # >40% of words gone
|
|
111
|
+
with patch("voiceio.llm_api.chat", return_value=gutted):
|
|
112
|
+
assert pc.correct(original) == original
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_disfluency_mode_rejects_rewording():
|
|
116
|
+
"""Many substitutions = a rewrite, not filler removal — reject."""
|
|
117
|
+
pc = PostCorrector(_cfg(remove_disfluencies=True))
|
|
118
|
+
original = "the server crashed at midnight during the backup"
|
|
119
|
+
reworded = "the machine went down at noon while copying files"
|
|
120
|
+
with patch("voiceio.llm_api.chat", return_value=reworded):
|
|
121
|
+
assert pc.correct(original) == original
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_fix_mode_still_rejects_big_deletion():
|
|
125
|
+
"""With disfluency mode OFF, the original conservative guards stand: a big
|
|
126
|
+
deletion is a length change and must be rejected."""
|
|
127
|
+
pc = PostCorrector(_cfg(remove_disfluencies=False))
|
|
128
|
+
original = "so um we should like ship the thing today"
|
|
129
|
+
cleaned = "so we should ship the thing today"
|
|
130
|
+
with patch("voiceio.llm_api.chat", return_value=cleaned):
|
|
131
|
+
assert pc.correct(original) == original
|
|
132
|
+
|
|
133
|
+
|
|
82
134
|
def test_llm_error_returns_original():
|
|
83
135
|
pc = PostCorrector(_cfg())
|
|
84
136
|
with patch("voiceio.llm_api.chat", side_effect=RuntimeError("boom")):
|
|
@@ -1,10 +1,56 @@
|
|
|
1
1
|
"""Tests for text post-processing (punctuation, capitalization)."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
from voiceio.postprocess import apply_pipeline, cleanup
|
|
4
|
+
from voiceio.postprocess import apply_pipeline, cleanup, strip_disfluencies
|
|
5
5
|
from voiceio.streaming import _word_match_len
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
class TestStripDisfluencies:
|
|
9
|
+
"""Delete-only regex layer: filler sounds + duplicate re-decode sentences,
|
|
10
|
+
never anything that could change meaning."""
|
|
11
|
+
|
|
12
|
+
def test_removes_filler_sounds(self):
|
|
13
|
+
out = strip_disfluencies("so um we should uh ship it")
|
|
14
|
+
assert "um" not in out.split()
|
|
15
|
+
assert "uh" not in out.split()
|
|
16
|
+
assert "we should" in out and "ship it" in out
|
|
17
|
+
|
|
18
|
+
def test_removes_comma_wrapped_filler(self):
|
|
19
|
+
assert "uh" not in strip_disfluencies("we tested, uh, everything").split()
|
|
20
|
+
|
|
21
|
+
def test_preserves_valid_word_repetition(self):
|
|
22
|
+
# "had had" is valid English; regex must NOT touch word repeats — that
|
|
23
|
+
# judgment belongs to the LLM layer.
|
|
24
|
+
assert strip_disfluencies("I had had enough") == "I had had enough"
|
|
25
|
+
|
|
26
|
+
def test_dedups_duplicate_sentence(self):
|
|
27
|
+
# The Whisper re-decode artifact: an exact sentence repeated.
|
|
28
|
+
out = strip_disfluencies("Do the research. Do the research.")
|
|
29
|
+
assert out == "Do the research."
|
|
30
|
+
|
|
31
|
+
def test_keeps_meaningful_words(self):
|
|
32
|
+
# 'like' as a real verb/preposition and content must survive.
|
|
33
|
+
text = "I like the design and it works like a charm"
|
|
34
|
+
assert strip_disfluencies(text) == text
|
|
35
|
+
|
|
36
|
+
def test_empty(self):
|
|
37
|
+
assert strip_disfluencies("") == ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TestPipelineDisfluencies:
|
|
41
|
+
def test_flag_off_keeps_fillers(self):
|
|
42
|
+
text, _ = apply_pipeline("um hello there friend", do_cleanup=True, final=True)
|
|
43
|
+
assert "um" in text.lower().split()
|
|
44
|
+
|
|
45
|
+
def test_flag_on_strips_fillers(self):
|
|
46
|
+
text, _ = apply_pipeline(
|
|
47
|
+
"um hello there friend",
|
|
48
|
+
do_cleanup=True, remove_disfluencies=True, final=True,
|
|
49
|
+
)
|
|
50
|
+
assert "um" not in text.lower().split()
|
|
51
|
+
assert "hello there friend" in text.lower()
|
|
52
|
+
|
|
53
|
+
|
|
8
54
|
class TestCapitalization:
|
|
9
55
|
def test_first_char(self):
|
|
10
56
|
assert cleanup("hello world") == "Hello world"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.9.6"
|
|
@@ -188,6 +188,7 @@ class VoiceIO:
|
|
|
188
188
|
self._command_processor = CommandProcessor(enabled=cfg.commands.enabled, editing=cfg.commands.editing)
|
|
189
189
|
self._cleanup = cfg.output.punctuation_cleanup
|
|
190
190
|
self._number_conversion = cfg.output.number_conversion
|
|
191
|
+
self._remove_disfluencies = cfg.output.remove_disfluencies
|
|
191
192
|
self._voice_input_prefix = cfg.output.voice_input_prefix
|
|
192
193
|
self._streaming = cfg.output.streaming
|
|
193
194
|
|
|
@@ -458,6 +459,7 @@ class VoiceIO:
|
|
|
458
459
|
self.transcriber, self._typer, self.recorder,
|
|
459
460
|
generation=self._generation,
|
|
460
461
|
cleanup=self._cleanup,
|
|
462
|
+
remove_disfluencies=self._remove_disfluencies,
|
|
461
463
|
number_conversion=self._number_conversion,
|
|
462
464
|
language=self.cfg.model.language,
|
|
463
465
|
commands=self._command_processor,
|
|
@@ -669,6 +671,7 @@ class VoiceIO:
|
|
|
669
671
|
text, abort = apply_pipeline(
|
|
670
672
|
text,
|
|
671
673
|
do_cleanup=self._cleanup,
|
|
674
|
+
remove_disfluencies=self._remove_disfluencies,
|
|
672
675
|
number_conversion=self._number_conversion,
|
|
673
676
|
language=self.cfg.model.language,
|
|
674
677
|
commands=self._command_processor,
|
|
@@ -88,6 +88,12 @@ class OutputConfig:
|
|
|
88
88
|
cancel_window_secs: float = 0.5
|
|
89
89
|
punctuation_cleanup: bool = True
|
|
90
90
|
number_conversion: bool = True
|
|
91
|
+
# Strip spoken disfluencies so dictation reads as writing. Delete-only and
|
|
92
|
+
# meaning-preserving: a free regex layer removes filler sounds (um/uh/er)
|
|
93
|
+
# and duplicate re-decode sentences on every pass; when [postcorrect] is
|
|
94
|
+
# also enabled, its LLM pass additionally removes false starts and filler
|
|
95
|
+
# "like", guarded so it can only delete — never add, rephrase, or reorder.
|
|
96
|
+
remove_disfluencies: bool = False
|
|
91
97
|
voice_input_prefix: str = "" # e.g. "[voice]" — empty disables
|
|
92
98
|
# Incremental finalization: once the un-finalized audio tail grows past
|
|
93
99
|
# this many seconds, it is cut at the nearest interior speech pause,
|
|
@@ -26,6 +26,14 @@ log = logging.getLogger(__name__)
|
|
|
26
26
|
_MAX_EDIT_RATIO = 0.3 # word-level SequenceMatcher edit ratio ceiling
|
|
27
27
|
_MAX_WORDCOUNT_DELTA = 0.2 # allowed relative change in word count
|
|
28
28
|
|
|
29
|
+
# Disfluency-mode guards. The whole promise is "never change meaning", enforced
|
|
30
|
+
# structurally on the word-level diff: no INSERTIONS (can't add or rephrase),
|
|
31
|
+
# few REPLACEMENTS (ASR fixes only, not reword), bounded DELETIONS (can't nuke
|
|
32
|
+
# real content). Deletions are what disfluency removal legitimately does.
|
|
33
|
+
_MAX_INSERTED_WORDS = 0 # adding any word = altering meaning → reject
|
|
34
|
+
_MAX_REPLACE_FRAC = 0.15 # ASR word-fixes only, never wholesale rewording
|
|
35
|
+
_MAX_DELETE_FRAC = 0.4 # backstop against deleting real content
|
|
36
|
+
|
|
29
37
|
_SYSTEM_PROMPT = (
|
|
30
38
|
"You fix automatic speech recognition errors in dictated text. "
|
|
31
39
|
"The user dictates about software engineering and their projects. "
|
|
@@ -35,6 +43,23 @@ _SYSTEM_PROMPT = (
|
|
|
35
43
|
"Return only the corrected text with no commentary."
|
|
36
44
|
)
|
|
37
45
|
|
|
46
|
+
# Disfluency mode: also strip spoken filler, delete-only. The strict rules
|
|
47
|
+
# mirror the guards — the model is told exactly what the diff check enforces.
|
|
48
|
+
_SYSTEM_PROMPT_CLEAN = (
|
|
49
|
+
"You convert dictated speech into clean written text. The user dictates "
|
|
50
|
+
"about software engineering and their projects. Do exactly two things:\n"
|
|
51
|
+
"1. Fix words the recognizer misheard (wrong proper nouns, homophones, "
|
|
52
|
+
"garbled technical terms).\n"
|
|
53
|
+
"2. Remove speech disfluencies: filler sounds (um, uh, er); filler uses of "
|
|
54
|
+
"'like', 'you know', 'I mean'; false starts and self-corrections (keep the "
|
|
55
|
+
"corrected version); and stray word repetitions.\n"
|
|
56
|
+
"STRICT RULES: Only DELETE disfluencies and FIX misheard words. NEVER add "
|
|
57
|
+
"words. NEVER rephrase, reword, reorder, or summarize. NEVER drop real "
|
|
58
|
+
"content, meaningful hedges, or negations. If unsure whether something is a "
|
|
59
|
+
"disfluency, KEEP it. Preserve the speaker's own wording and punctuation. "
|
|
60
|
+
"Return only the cleaned text, nothing else."
|
|
61
|
+
)
|
|
62
|
+
|
|
38
63
|
_MAX_RECENT = 3
|
|
39
64
|
|
|
40
65
|
|
|
@@ -82,6 +107,7 @@ class PostCorrector:
|
|
|
82
107
|
def __init__(self, cfg: Config):
|
|
83
108
|
self._cfg = cfg
|
|
84
109
|
self._pc = cfg.postcorrect
|
|
110
|
+
self._remove_disfluencies = cfg.output.remove_disfluencies
|
|
85
111
|
# API key / base_url resolution is shared with [autocorrect].
|
|
86
112
|
self._ac = cfg.autocorrect
|
|
87
113
|
self._available: bool | None = None
|
|
@@ -210,10 +236,14 @@ class PostCorrector:
|
|
|
210
236
|
t0 = time.monotonic()
|
|
211
237
|
outcome: dict = {}
|
|
212
238
|
|
|
239
|
+
system_prompt = (
|
|
240
|
+
_SYSTEM_PROMPT_CLEAN if self._remove_disfluencies else _SYSTEM_PROMPT
|
|
241
|
+
)
|
|
242
|
+
|
|
213
243
|
def _call() -> None:
|
|
214
244
|
try:
|
|
215
245
|
outcome["response"] = chat(
|
|
216
|
-
self._client_cfg(),
|
|
246
|
+
self._client_cfg(), system_prompt, user_msg, max_tokens=1024,
|
|
217
247
|
)
|
|
218
248
|
except Exception as e:
|
|
219
249
|
outcome["error"] = e
|
|
@@ -251,26 +281,47 @@ class PostCorrector:
|
|
|
251
281
|
self._record(text, corrected, "unchanged")
|
|
252
282
|
return text
|
|
253
283
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
"PostCorrector reject: word count %d→%d (>%.0f%%) — keeping original",
|
|
259
|
-
orig_wc, new_wc, _MAX_WORDCOUNT_DELTA * 100,
|
|
260
|
-
)
|
|
261
|
-
self._record(text, corrected, "rejected_wordcount")
|
|
262
|
-
return text
|
|
263
|
-
|
|
264
|
-
# Guard: only a small fraction of words may change.
|
|
265
|
-
ratio = _word_edit_ratio(text, corrected)
|
|
266
|
-
if ratio > _MAX_EDIT_RATIO:
|
|
267
|
-
log.debug(
|
|
268
|
-
"PostCorrector reject: edit ratio %.2f > %.2f — keeping original",
|
|
269
|
-
ratio, _MAX_EDIT_RATIO,
|
|
270
|
-
)
|
|
271
|
-
self._record(text, corrected, "rejected_editratio")
|
|
284
|
+
accept, reject_reason = self._guard(text, corrected)
|
|
285
|
+
if not accept:
|
|
286
|
+
log.debug("PostCorrector reject (%s) — keeping original", reject_reason)
|
|
287
|
+
self._record(text, corrected, f"rejected_{reject_reason}")
|
|
272
288
|
return text
|
|
273
289
|
|
|
274
290
|
log.info("PostCorrector fixed: %s", ", ".join(_changed_words(text, corrected)))
|
|
275
291
|
self._record(text, corrected, "applied")
|
|
276
292
|
return corrected
|
|
293
|
+
|
|
294
|
+
def _guard(self, text: str, corrected: str) -> tuple[bool, str]:
|
|
295
|
+
"""Decide whether the LLM's edit is within bounds.
|
|
296
|
+
|
|
297
|
+
Returns (accept, reject_reason). In disfluency mode the promise is
|
|
298
|
+
"never change meaning", enforced on the word-level diff: zero
|
|
299
|
+
insertions (nothing added/rephrased), few replacements (ASR fixes, not
|
|
300
|
+
rewording), bounded deletions (can't strip real content). Otherwise the
|
|
301
|
+
original conservative fix-only guards apply.
|
|
302
|
+
"""
|
|
303
|
+
aw, bw = text.split(), corrected.split()
|
|
304
|
+
if not self._remove_disfluencies:
|
|
305
|
+
n = len(aw)
|
|
306
|
+
if n and abs(len(bw) - n) / n > _MAX_WORDCOUNT_DELTA:
|
|
307
|
+
return False, "wordcount"
|
|
308
|
+
if _word_edit_ratio(text, corrected) > _MAX_EDIT_RATIO:
|
|
309
|
+
return False, "editratio"
|
|
310
|
+
return True, ""
|
|
311
|
+
|
|
312
|
+
inserted = replaced = deleted = 0
|
|
313
|
+
for tag, i1, i2, j1, j2 in difflib.SequenceMatcher(a=aw, b=bw).get_opcodes():
|
|
314
|
+
if tag == "insert":
|
|
315
|
+
inserted += j2 - j1
|
|
316
|
+
elif tag == "replace":
|
|
317
|
+
replaced += max(i2 - i1, j2 - j1)
|
|
318
|
+
elif tag == "delete":
|
|
319
|
+
deleted += i2 - i1
|
|
320
|
+
n = len(aw)
|
|
321
|
+
if inserted > _MAX_INSERTED_WORDS:
|
|
322
|
+
return False, "inserted" # added content — meaning changed
|
|
323
|
+
if replaced > max(3, int(_MAX_REPLACE_FRAC * n)):
|
|
324
|
+
return False, "reworded" # too many substitutions = rewrite
|
|
325
|
+
if n and deleted > _MAX_DELETE_FRAC * n:
|
|
326
|
+
return False, "overdeleted" # nuked real content
|
|
327
|
+
return True, ""
|
|
@@ -13,6 +13,43 @@ if TYPE_CHECKING:
|
|
|
13
13
|
# Languages that don't use letter casing
|
|
14
14
|
_NO_CASE_LANGUAGES = frozenset({"zh", "ja", "ko", "ar", "he", "th", "hi", "bn", "ka", "my"})
|
|
15
15
|
|
|
16
|
+
# Filler SOUNDS only — tokens with no lexical meaning, so deleting them can
|
|
17
|
+
# never change meaning. Word repetitions ("had had" is valid English) and
|
|
18
|
+
# filler uses of "like"/"you know" need judgment and are left to the LLM layer.
|
|
19
|
+
# Consumes an adjacent comma on either side so "be, uh, found" → "be found".
|
|
20
|
+
_FILLER_RE = re.compile(
|
|
21
|
+
r"\s*,?\s*\b(?:u+m+|u+h+m*|e+r+m*|erm+|a+h+|h+m+|mm+|mhm|uh[-\s]?huh)\b\s*,?\s*",
|
|
22
|
+
re.IGNORECASE,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def strip_disfluencies(text: str) -> str:
|
|
27
|
+
"""Delete-only, meaning-safe disfluency cleanup (regex layer).
|
|
28
|
+
|
|
29
|
+
Removes filler sounds (um, uh, er, …) and exact duplicate adjacent
|
|
30
|
+
sentences (a Whisper re-decode artifact). Only ever deletes — never
|
|
31
|
+
rephrases, reorders, or touches lexical words — so meaning is preserved by
|
|
32
|
+
construction. The judgment cases (false starts, filler "like", word
|
|
33
|
+
repetitions) are handled by the guarded LLM layer in postcorrect.
|
|
34
|
+
"""
|
|
35
|
+
if not text:
|
|
36
|
+
return text
|
|
37
|
+
text = _FILLER_RE.sub(" ", text)
|
|
38
|
+
text = _dedup_adjacent_sentences(text)
|
|
39
|
+
text = re.sub(r"\s{2,}", " ", text)
|
|
40
|
+
return text.strip()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _dedup_adjacent_sentences(text: str) -> str:
|
|
44
|
+
"""Drop a sentence identical to the one immediately before it."""
|
|
45
|
+
parts = re.split(r"(?<=[.?!])\s+", text)
|
|
46
|
+
out: list[str] = []
|
|
47
|
+
for p in parts:
|
|
48
|
+
if out and p.strip().lower() == out[-1].strip().lower():
|
|
49
|
+
continue
|
|
50
|
+
out.append(p)
|
|
51
|
+
return " ".join(out)
|
|
52
|
+
|
|
16
53
|
|
|
17
54
|
def cleanup(text: str, language: str = "en") -> str:
|
|
18
55
|
"""Clean up Whisper output. Idempotent — safe to run multiple times."""
|
|
@@ -50,6 +87,7 @@ def apply_pipeline(
|
|
|
50
87
|
text: str,
|
|
51
88
|
*,
|
|
52
89
|
do_cleanup: bool = False,
|
|
90
|
+
remove_disfluencies: bool = False,
|
|
53
91
|
number_conversion: bool = False,
|
|
54
92
|
language: str = "en",
|
|
55
93
|
commands: CommandProcessor | None = None,
|
|
@@ -64,6 +102,11 @@ def apply_pipeline(
|
|
|
64
102
|
Returns (processed_text, abort). If abort is True, the caller should
|
|
65
103
|
discard the result (e.g. undo/flag command was triggered).
|
|
66
104
|
"""
|
|
105
|
+
# Disfluency removal runs BEFORE cleanup so cleanup re-fixes the spacing,
|
|
106
|
+
# commas, and capitalization the deletions leave behind. Delete-only.
|
|
107
|
+
if remove_disfluencies:
|
|
108
|
+
text = strip_disfluencies(text)
|
|
109
|
+
|
|
67
110
|
if do_cleanup:
|
|
68
111
|
text = cleanup(text, language)
|
|
69
112
|
|
|
@@ -135,6 +135,7 @@ class StreamingSession:
|
|
|
135
135
|
recorder: AudioRecorder,
|
|
136
136
|
generation: int = 0,
|
|
137
137
|
cleanup: bool = False,
|
|
138
|
+
remove_disfluencies: bool = False,
|
|
138
139
|
number_conversion: bool = False,
|
|
139
140
|
language: str = "en",
|
|
140
141
|
commands: CommandProcessor | None = None,
|
|
@@ -158,6 +159,7 @@ class StreamingSession:
|
|
|
158
159
|
self._sample_rate = recorder.sample_rate
|
|
159
160
|
self._generation = generation
|
|
160
161
|
self._cleanup = cleanup
|
|
162
|
+
self._remove_disfluencies = remove_disfluencies
|
|
161
163
|
self._number_conversion = number_conversion
|
|
162
164
|
self._language = language
|
|
163
165
|
self._commands = commands
|
|
@@ -450,6 +452,7 @@ class StreamingSession:
|
|
|
450
452
|
text, abort = apply_pipeline(
|
|
451
453
|
raw,
|
|
452
454
|
do_cleanup=self._cleanup,
|
|
455
|
+
remove_disfluencies=self._remove_disfluencies,
|
|
453
456
|
number_conversion=self._number_conversion,
|
|
454
457
|
language=self._language,
|
|
455
458
|
commands=self._commands,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.9.5"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|