main
py 234 lines 9.35 KB
Raw
1 from __future__ import annotations
2
3 import importlib
4 import sys
5 import types
6 from pathlib import Path
7
8
9 PROJECT_ROOT = Path(__file__).resolve().parents[1]
10 if str(PROJECT_ROOT) not in sys.path:
11 sys.path.insert(0, str(PROJECT_ROOT))
12
13
14 from helpers import plugins, settings
15
16
17 def test_builtin_speech_plugins_are_discoverable_and_toggleable() -> None:
18 discovered = {
19 item.name: item
20 for item in plugins.get_enhanced_plugins_list(
21 custom=True,
22 builtin=True,
23 plugin_names=["_kokoro_tts", "_whisper_stt"],
24 )
25 }
26
27 assert "_kokoro_tts" in discovered
28 assert "_whisper_stt" in discovered
29
30 assert discovered["_kokoro_tts"].always_enabled is False
31 assert discovered["_whisper_stt"].always_enabled is False
32 assert "agent" in discovered["_kokoro_tts"].settings_sections
33 assert "agent" in discovered["_whisper_stt"].settings_sections
34
35
36 def test_legacy_core_speech_artifacts_are_removed() -> None:
37 removed_paths = [
38 "api/synthesize.py",
39 "api/transcribe.py",
40 "helpers/kokoro_tts.py",
41 "helpers/whisper.py",
42 "webui/components/chat/speech/speech-store.js",
43 "webui/components/settings/agent/speech.html",
44 "webui/components/settings/speech/microphone-setting-store.js",
45 "webui/components/settings/speech/microphone.html",
46 "webui/css/speech.css",
47 "webui/js/speech_browser.js",
48 ]
49
50 for relative_path in removed_paths:
51 assert not (PROJECT_ROOT / relative_path).exists(), relative_path
52
53
54 def test_plugin_owned_voice_files_exist() -> None:
55 expected_paths = [
56 "plugins/_kokoro_tts/plugin.yaml",
57 "plugins/_kokoro_tts/api/synthesize.py",
58 "plugins/_kokoro_tts/extensions/webui/page-head/runtime.html",
59 "plugins/_kokoro_tts/extensions/webui/voice-settings-main/kokoro-card.html",
60 "plugins/_whisper_stt/plugin.yaml",
61 "plugins/_whisper_stt/api/transcribe.py",
62 "plugins/_whisper_stt/extensions/webui/page-head/runtime.html",
63 "plugins/_whisper_stt/extensions/webui/chat-input-box-end/microphone-button.html",
64 "plugins/_whisper_stt/extensions/webui/voice-settings-main/whisper-card.html",
65 "plugins/_whisper_stt/webui/whisper-stt-store.js",
66 ]
67
68 for relative_path in expected_paths:
69 assert (PROJECT_ROOT / relative_path).exists(), relative_path
70
71
72 def test_core_settings_no_longer_expose_legacy_speech_keys() -> None:
73 defaults = settings.get_default_settings()
74 output = settings.convert_out(defaults)
75
76 legacy_keys = {
77 "tts_kokoro",
78 "stt_model_size",
79 "stt_language",
80 "stt_silence_threshold",
81 "stt_silence_duration",
82 "stt_waiting_timeout",
83 }
84
85 assert legacy_keys.isdisjoint(defaults.keys())
86 assert legacy_keys.isdisjoint(output["settings"].keys())
87 assert "stt_models" not in output["additional"]
88
89
90 def test_voice_prefix_prompt_rule_is_removed() -> None:
91 core_prompt = (PROJECT_ROOT / "prompts/agent.system.main.communication_additions.md").read_text(
92 encoding="utf-8"
93 )
94 whisper_store = (
95 PROJECT_ROOT / "plugins/_whisper_stt/webui/whisper-stt-store.js"
96 ).read_text(encoding="utf-8")
97 voice_surface = (PROJECT_ROOT / "webui/components/settings/agent/voice.html").read_text(
98 encoding="utf-8"
99 )
100
101 assert "if starts (voice) then transcribed can contain errors consider compensation" not in core_prompt
102 assert "(voice)" not in whisper_store
103 assert not (
104 PROJECT_ROOT / "plugins/_whisper_stt/prompts/agent.system.voice_transcription.md"
105 ).exists()
106 assert not (
107 PROJECT_ROOT
108 / "plugins/_whisper_stt/extensions/python/system_prompt/_20_voice_transcription.py"
109 ).exists()
110 assert '<x-extension id="voice-settings-start"></x-extension>' in voice_surface
111 assert '<x-extension id="voice-settings-main"></x-extension>' in voice_surface
112 assert '<x-extension id="voice-settings-end"></x-extension>' in voice_surface
113
114
115 def test_whisper_message_mode_defaults_to_send_and_supports_draft() -> None:
116 sys.modules.setdefault(
117 "whisper",
118 types.SimpleNamespace(load_model=lambda *args, **kwargs: None),
119 )
120 runtime = importlib.import_module("plugins._whisper_stt.helpers.runtime")
121
122 assert runtime.normalize_config({})["message_mode"] == "send"
123 assert runtime.normalize_config({"message_mode": "draft"})["message_mode"] == "draft"
124 assert runtime.normalize_config({"message_mode": "DRAFT"})["message_mode"] == "draft"
125 assert runtime.normalize_config({"message_mode": "invalid"})["message_mode"] == "send"
126
127 default_config = (
128 PROJECT_ROOT / "plugins/_whisper_stt/default_config.yaml"
129 ).read_text(encoding="utf-8")
130 migration = (
131 PROJECT_ROOT / "plugins/_whisper_stt/helpers/migration.py"
132 ).read_text(encoding="utf-8")
133 config_ui = (
134 PROJECT_ROOT / "plugins/_whisper_stt/webui/config.html"
135 ).read_text(encoding="utf-8")
136 status_ui = (
137 PROJECT_ROOT / "plugins/_whisper_stt/webui/main.html"
138 ).read_text(encoding="utf-8")
139 voice_card = (
140 PROJECT_ROOT
141 / "plugins/_whisper_stt/extensions/webui/voice-settings-main/whisper-card.html"
142 ).read_text(encoding="utf-8")
143 whisper_store = (
144 PROJECT_ROOT / "plugins/_whisper_stt/webui/whisper-stt-store.js"
145 ).read_text(encoding="utf-8")
146
147 assert "message_mode: send" in default_config
148 assert '"message_mode": "send"' in migration
149 assert '<option value="send">Send immediately</option>' in config_ui
150 assert '<option value="draft">Draft in composer</option>' in config_ui
151 assert "messageModeLabel" in status_ui
152 assert "messageModeLabel" in voice_card
153 assert 'message_mode: "send"' in whisper_store
154 assert 'status?.config?.message_mode === "draft" ? "draft" : "send"' in whisper_store
155 assert "updateChatInput(message)" in whisper_store
156 assert "sendMessage()" in whisper_store
157
158
159 def test_browser_tool_speech_action_uses_shared_tts_service() -> None:
160 browser_handler = (
161 PROJECT_ROOT
162 / "plugins/_browser/extensions/webui/get_tool_message_handler/browser-tool-handler.js"
163 ).read_text(encoding="utf-8")
164
165 assert "/components/chat/speech/speech-store.js" not in browser_handler
166 assert "/js/tts-service.js" in browser_handler
167 assert "ttsService.speak(contentText)" in browser_handler
168
169
170 def test_chat_bar_keeps_existing_send_and_mic_icon_contract() -> None:
171 chat_bar = (
172 PROJECT_ROOT / "webui/components/chat/input/chat-bar-input.html"
173 ).read_text(encoding="utf-8")
174 mic_extension = (
175 PROJECT_ROOT
176 / "plugins/_whisper_stt/extensions/webui/chat-input-box-end/microphone-button.html"
177 ).read_text(encoding="utf-8")
178 whisper_store = (
179 PROJECT_ROOT / "plugins/_whisper_stt/webui/whisper-stt-store.js"
180 ).read_text(encoding="utf-8")
181 whisper_css = (
182 PROJECT_ROOT / "plugins/_whisper_stt/webui/whisper-stt.css"
183 ).read_text(encoding="utf-8")
184
185 assert 'id="send-button"' in chat_bar
186 assert ':name="$store.chatInput.sendButtonIcon"' in chat_bar
187 assert ':class="$store.chatInput.sendButtonClass"' in chat_bar
188 assert ':title="$store.chatInput.sendButtonTitle"' in chat_bar
189
190 assert 'id="microphone-button"' in mic_extension
191 assert "data-whisper-microphone" in mic_extension
192 assert "<svg" in mic_extension
193 assert "material-symbols-outlined" not in mic_extension
194 assert "buttonIcon" not in mic_extension
195 assert 'title=' not in mic_extension
196 assert 'x-effect="$store.whisperStt.updateMicrophoneButtonUI()"' in mic_extension
197 assert 'x-init="$store.whisperStt.updateMicrophoneButtonUI()"' in mic_extension
198 assert "updateMicrophoneButtonUI()" in whisper_store
199 assert "data-status" in whisper_store
200 assert 'setAttribute("title"' not in whisper_store
201 assert 'removeAttribute("title")' in whisper_store
202 assert 'removeAttribute("data-bs-original-title")' in whisper_store
203 assert "this.updateMicrophoneButtonUI();" in whisper_store
204 assert "sttService.emitStatusChange(this.micStatus)" in whisper_store
205
206 for state in [
207 "disabled",
208 "inactive",
209 "activating",
210 "listening",
211 "recording",
212 "waiting",
213 "processing",
214 ]:
215 assert f'"mic-{state}"' in whisper_store
216 assert f"#microphone-button.mic-{state}" in whisper_css
217 assert f"#microphone-button.mic-{state}" in mic_extension
218
219 assert "border-radius: 12px;" in whisper_css
220 assert "background-color: transparent;" in whisper_css
221 assert "#microphone-button.mic-inactive {\n color: grey;" in whisper_css
222 assert "background-color: grey;" not in whisper_css
223 assert "#microphone-button.mic-listening {\n color: red;" in whisper_css
224 assert "#microphone-button.mic-recording {\n color: green;" in whisper_css
225 assert "#microphone-button.mic-waiting {\n color: teal;" in whisper_css
226 assert "#microphone-button.mic-processing {\n color: darkcyan;" in whisper_css
227 assert "#microphone-button.mic-activating svg" in whisper_css
228 assert "#microphone-button.mic-processing svg" in whisper_css
229 assert "#microphone-button:not(.mic-disabled):hover svg" in whisper_css
230 assert "#microphone-button:not(.mic-disabled):active svg" in whisper_css
231 assert "box-shadow: none;" in whisper_css
232 assert "order: 1;" in mic_extension
233 assert "whisper-stt-mic-pulse 0.8s infinite" in mic_extension
234 assert "whisper-stt-mic-pulse 0.8s infinite" in whisper_css