blitztext-app-linux/linux/tests/test_voice_send.py
mARTin-B78 28020fdeda routing: send-by-voice keyword (types + presses Enter); wakeword benchmark via TTS; Release 1.8.0
- Send by voice: a configured edge-anchored phrase (e.g. "computer send") is
  stripped and the rest is delivered AND submitted with Enter. Off by default;
  [routing] send_keywords + Settings → Input.
- Wakeword benchmark (Settings → Benchmark): synthesize the wake phrase in
  random voices via any OpenAI-compatible TTS server, stream to
  wyoming-openwakeword, report recall / false-fires / per-voice breakdown.
  New [tts] config block.
- Tests: test_voice_send.py, test_wakeword_bench.py.
- Ignore agent workspace folder jules/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:17:33 +02:00

84 lines
3.8 KiB
Python

"""Spoken send: a configured word delivers the clip AND presses Enter.
The spoken equivalent of stop+paste+Enter — mainly hands-free. The keyword is
stripped from the transcript before the rest is delivered and submitted.
"""
import blitztext.daemon as dm
import blitztext.paste as paste
from blitztext.config import Config, Workflow
from blitztext.daemon import Daemon
from blitztext.routing import match_send
# -- matcher --------------------------------------------------------------------
def test_match_send_strips_edges_only():
kws = ["computer send", "computer abschicken"]
assert match_send("computer send", kws)[0] == "computer send" # whole utterance
kw, text = match_send("hey team the build is green computer send", kws)
assert kw == "computer send" and text == "hey team the build is green" # end edge, stripped
kw, text = match_send("computer send hey team the build is green", kws)
assert kw == "computer send" and text == "hey team the build is green" # start edge, stripped
# A bare word inside a sentence must NOT submit — the phrase is distinctive.
assert match_send("please send me the report tomorrow", kws)[0] is None
assert match_send("computer send", [])[0] is None # disabled
assert match_send("", kws)[0] is None
# -- pipeline -------------------------------------------------------------------
def _wire_clean_pipeline(monkeypatch):
monkeypatch.setattr(dm, "detect_recorder", lambda pref="auto": "pw-record")
monkeypatch.setattr(dm, "notify", lambda *a, **k: None)
monkeypatch.setattr(dm.quality, "analyze_wav", lambda p: (2.0, 0.5))
monkeypatch.setattr(dm.quality, "too_quiet", lambda *a, **k: False)
monkeypatch.setattr(dm.quality, "clean", lambda t, **k: t)
monkeypatch.setattr(dm.quality, "is_hallucination", lambda *a, **k: False)
delivered = []
monkeypatch.setattr(dm, "deliver", lambda *a, **k: delivered.append((a, k)))
enters = []
monkeypatch.setattr(paste, "press_enter", lambda win=None: enters.append(win))
return delivered, enters
def test_process_sends_with_enter_when_send_spoken(monkeypatch, tmp_path):
delivered, enters = _wire_clean_pipeline(monkeypatch)
monkeypatch.setattr(dm.stt, "transcribe", lambda *a, **k: "die nachricht ist fertig computer send")
d = Daemon(Config())
d._prepared = True
d.cfg.send_keywords = ["computer send"]
d.cfg.cancel_keywords = []
audio = tmp_path / "clip.wav"; audio.write_bytes(b"x")
d._process(audio, Workflow(name="Transcribe", hotkey="", mode="transcribe"), None)
assert len(delivered) == 1
assert delivered[0][0][0] == "die nachricht ist fertig" # keyword stripped
assert len(enters) == 1, "send keyword must press Enter"
def test_process_no_enter_without_send_word(monkeypatch, tmp_path):
"""Control: the same path without a send word delivers but never hits Enter."""
delivered, enters = _wire_clean_pipeline(monkeypatch)
monkeypatch.setattr(dm.stt, "transcribe", lambda *a, **k: "die nachricht ist fertig")
d = Daemon(Config())
d._prepared = True
d.cfg.send_keywords = ["computer send"]
d.cfg.cancel_keywords = []
audio = tmp_path / "clip.wav"; audio.write_bytes(b"x")
d._process(audio, Workflow(name="Transcribe", hotkey="", mode="transcribe"), None)
assert len(delivered) == 1
assert delivered[0][0][0] == "die nachricht ist fertig"
assert enters == [], "no send word → no Enter"
def test_send_keywords_round_trip(tmp_path):
from blitztext.config import load, save
p = tmp_path / "config.toml"
cfg = load(p)
assert cfg.send_keywords == [] # shipped default: off
cfg.send_keywords = ["computer send", "computer abschicken"]
save(cfg, p)
assert load(p).send_keywords == ["computer send", "computer abschicken"]