openvoiceui 1.0.0
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.
- package/.env.example +104 -0
- package/Dockerfile +30 -0
- package/LICENSE +21 -0
- package/README.md +638 -0
- package/SETUP.md +360 -0
- package/app.py +232 -0
- package/auto-approve-devices.js +111 -0
- package/cli/index.js +372 -0
- package/config/__init__.py +4 -0
- package/config/default.yaml +43 -0
- package/config/flags.yaml +67 -0
- package/config/loader.py +203 -0
- package/config/providers.yaml +71 -0
- package/config/speech_normalization.yaml +182 -0
- package/config/theme.json +4 -0
- package/data/greetings.json +25 -0
- package/default-pages/ai-image-creator.html +915 -0
- package/default-pages/bulk-image-uploader.html +492 -0
- package/default-pages/desktop.html +2865 -0
- package/default-pages/file-explorer.html +854 -0
- package/default-pages/interactive-map.html +655 -0
- package/default-pages/style-guide.html +1005 -0
- package/default-pages/website-setup.html +1623 -0
- package/deploy/openclaw/Dockerfile +46 -0
- package/deploy/openvoiceui.service +30 -0
- package/deploy/setup-nginx.sh +50 -0
- package/deploy/setup-sudo.sh +306 -0
- package/deploy/skill-runner/Dockerfile +19 -0
- package/deploy/skill-runner/requirements.txt +14 -0
- package/deploy/skill-runner/server.py +269 -0
- package/deploy/supertonic/Dockerfile +22 -0
- package/deploy/supertonic/server.py +79 -0
- package/docker-compose.pinokio.yml +11 -0
- package/docker-compose.yml +59 -0
- package/greetings.json +25 -0
- package/index.html +65 -0
- package/inject-device-identity.js +142 -0
- package/package.json +82 -0
- package/profiles/default.json +114 -0
- package/profiles/manager.py +354 -0
- package/profiles/schema.json +337 -0
- package/prompts/voice-system-prompt.md +149 -0
- package/providers/__init__.py +39 -0
- package/providers/base.py +63 -0
- package/providers/llm/__init__.py +12 -0
- package/providers/llm/base.py +71 -0
- package/providers/llm/clawdbot_provider.py +112 -0
- package/providers/llm/zai_provider.py +115 -0
- package/providers/registry.py +320 -0
- package/providers/stt/__init__.py +12 -0
- package/providers/stt/base.py +58 -0
- package/providers/stt/webspeech_provider.py +49 -0
- package/providers/stt/whisper_provider.py +100 -0
- package/providers/tts/__init__.py +20 -0
- package/providers/tts/base.py +91 -0
- package/providers/tts/groq_provider.py +74 -0
- package/providers/tts/supertonic_provider.py +72 -0
- package/requirements.txt +38 -0
- package/routes/__init__.py +10 -0
- package/routes/admin.py +515 -0
- package/routes/canvas.py +1315 -0
- package/routes/chat.py +51 -0
- package/routes/conversation.py +2158 -0
- package/routes/elevenlabs_hybrid.py +306 -0
- package/routes/greetings.py +98 -0
- package/routes/icons.py +279 -0
- package/routes/image_gen.py +364 -0
- package/routes/instructions.py +190 -0
- package/routes/music.py +838 -0
- package/routes/onboarding.py +43 -0
- package/routes/pi.py +62 -0
- package/routes/profiles.py +215 -0
- package/routes/report_issue.py +68 -0
- package/routes/static_files.py +533 -0
- package/routes/suno.py +664 -0
- package/routes/theme.py +81 -0
- package/routes/transcripts.py +199 -0
- package/routes/vision.py +348 -0
- package/routes/workspace.py +288 -0
- package/server.py +1510 -0
- package/services/__init__.py +1 -0
- package/services/auth.py +143 -0
- package/services/canvas_versioning.py +239 -0
- package/services/db_pool.py +107 -0
- package/services/gateway.py +16 -0
- package/services/gateway_manager.py +333 -0
- package/services/gateways/__init__.py +12 -0
- package/services/gateways/base.py +110 -0
- package/services/gateways/compat.py +264 -0
- package/services/gateways/openclaw.py +1134 -0
- package/services/health.py +100 -0
- package/services/memory_client.py +455 -0
- package/services/paths.py +26 -0
- package/services/speech_normalizer.py +285 -0
- package/services/tts.py +270 -0
- package/setup-config.js +262 -0
- package/sounds/air_horn.mp3 +0 -0
- package/sounds/bruh.mp3 +0 -0
- package/sounds/crowd_cheer.mp3 +0 -0
- package/sounds/gunshot.mp3 +0 -0
- package/sounds/impact.mp3 +0 -0
- package/sounds/lets_go.mp3 +0 -0
- package/sounds/record_stop.mp3 +0 -0
- package/sounds/rewind.mp3 +0 -0
- package/sounds/sad_trombone.mp3 +0 -0
- package/sounds/scratch_long.mp3 +0 -0
- package/sounds/yeah.mp3 +0 -0
- package/src/adapters/ClawdBotAdapter.js +264 -0
- package/src/adapters/_template.js +133 -0
- package/src/adapters/elevenlabs-classic.js +841 -0
- package/src/adapters/elevenlabs-hybrid.js +812 -0
- package/src/adapters/hume-evi.js +676 -0
- package/src/admin.html +1339 -0
- package/src/app.js +8802 -0
- package/src/core/Config.js +173 -0
- package/src/core/EmotionEngine.js +307 -0
- package/src/core/EventBridge.js +180 -0
- package/src/core/EventBus.js +117 -0
- package/src/core/VoiceSession.js +607 -0
- package/src/face/BaseFace.js +259 -0
- package/src/face/EyeFace.js +208 -0
- package/src/face/HaloSmokeFace.js +509 -0
- package/src/face/manifest.json +27 -0
- package/src/face/previews/eyes.svg +16 -0
- package/src/face/previews/orb.svg +29 -0
- package/src/features/MusicPlayer.js +620 -0
- package/src/features/Soundboard.js +128 -0
- package/src/providers/DeepgramSTT.js +472 -0
- package/src/providers/DeepgramStreamingSTT.js +766 -0
- package/src/providers/GroqSTT.js +559 -0
- package/src/providers/TTSPlayer.js +323 -0
- package/src/providers/WebSpeechSTT.js +479 -0
- package/src/providers/tts/BaseTTSProvider.js +81 -0
- package/src/providers/tts/HumeProvider.js +77 -0
- package/src/providers/tts/SupertonicProvider.js +174 -0
- package/src/providers/tts/index.js +140 -0
- package/src/shell/adapter-registry.js +154 -0
- package/src/shell/caller-bridge.js +35 -0
- package/src/shell/camera-bridge.js +28 -0
- package/src/shell/canvas-bridge.js +32 -0
- package/src/shell/commercial-bridge.js +44 -0
- package/src/shell/face-bridge.js +44 -0
- package/src/shell/music-bridge.js +60 -0
- package/src/shell/orchestrator.js +233 -0
- package/src/shell/profile-discovery.js +303 -0
- package/src/shell/sounds-bridge.js +28 -0
- package/src/shell/transcript-bridge.js +61 -0
- package/src/shell/waveform-bridge.js +33 -0
- package/src/styles/base.css +2862 -0
- package/src/styles/face.css +417 -0
- package/src/styles/pi-overrides.css +89 -0
- package/src/styles/theme-dark.css +67 -0
- package/src/test-tts.html +175 -0
- package/src/ui/AppShell.js +544 -0
- package/src/ui/ProfileSwitcher.js +228 -0
- package/src/ui/SessionControl.js +240 -0
- package/src/ui/face/FacePicker.js +195 -0
- package/src/ui/face/FaceRenderer.js +309 -0
- package/src/ui/settings/PlaylistEditor.js +366 -0
- package/src/ui/settings/SettingsPanel.css +684 -0
- package/src/ui/settings/SettingsPanel.js +419 -0
- package/src/ui/settings/TTSVoicePreview.js +210 -0
- package/src/ui/themes/ThemeManager.js +213 -0
- package/src/ui/visualizers/BaseVisualizer.js +29 -0
- package/src/ui/visualizers/PartyFXVisualizer.css +291 -0
- package/src/ui/visualizers/PartyFXVisualizer.js +637 -0
- package/static/emulators/jsdos/js-dos.css +1 -0
- package/static/emulators/jsdos/js-dos.js +22 -0
- package/static/favicon.svg +55 -0
- package/static/icons/apple-touch-icon.png +0 -0
- package/static/icons/favicon-32.png +0 -0
- package/static/icons/icon-192.png +0 -0
- package/static/icons/icon-512.png +0 -0
- package/static/install.html +449 -0
- package/static/manifest.json +26 -0
- package/static/sw.js +21 -0
- package/tts_providers/__init__.py +136 -0
- package/tts_providers/base_provider.py +319 -0
- package/tts_providers/groq_provider.py +155 -0
- package/tts_providers/hume_provider.py +226 -0
- package/tts_providers/providers_config.json +119 -0
- package/tts_providers/qwen3_provider.py +371 -0
- package/tts_providers/resemble_provider.py +315 -0
- package/tts_providers/supertonic_provider.py +557 -0
- package/tts_providers/supertonic_tts.py +399 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenClaw protocol compatibility layer.
|
|
3
|
+
|
|
4
|
+
Centralizes all version-sensitive constants, event names, and protocol
|
|
5
|
+
details so that upgrading OpenClaw requires changing ONE file instead of
|
|
6
|
+
hunting through 4+ modules.
|
|
7
|
+
|
|
8
|
+
When OpenClaw bumps protocol versions or renames events, update the
|
|
9
|
+
mappings here and all consumers benefit automatically.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
import logging
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Protocol version negotiation
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
# Supported protocol versions — accept a range so minor bumps don't break us.
|
|
22
|
+
# OpenClaw negotiates the highest mutually supported version.
|
|
23
|
+
PROTOCOL_MIN = 3
|
|
24
|
+
PROTOCOL_MAX = 5 # forward-compatible — OpenClaw ignores unsupported maxes
|
|
25
|
+
|
|
26
|
+
# Version this code was tested against (for warning logs).
|
|
27
|
+
OPENCLAW_TESTED_VERSION = "2026.3.13"
|
|
28
|
+
OPENCLAW_MIN_VERSION = "2026.3.1"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Event name aliases — maps canonical names to sets of accepted alternatives.
|
|
33
|
+
# When OpenClaw renames an event, add the new name here.
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
# Top-level event types (data['event'] field)
|
|
37
|
+
_EVENT_ALIASES = {
|
|
38
|
+
'agent': frozenset({'agent', 'run', 'agent.run'}),
|
|
39
|
+
'chat': frozenset({'chat', 'conversation', 'chat.state'}),
|
|
40
|
+
'connect.challenge': frozenset({'connect.challenge', 'auth.challenge'}),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Agent stream types (payload['stream'] field)
|
|
44
|
+
_STREAM_ALIASES = {
|
|
45
|
+
'assistant': frozenset({'assistant', 'text', 'message'}),
|
|
46
|
+
'tool': frozenset({'tool', 'tool_use', 'tool-use'}),
|
|
47
|
+
'lifecycle': frozenset({'lifecycle', 'status', 'run.status'}),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Chat state values (payload['state'] field)
|
|
51
|
+
_STATE_ALIASES = {
|
|
52
|
+
'final': frozenset({'final', 'complete', 'done', 'finished'}),
|
|
53
|
+
'aborted': frozenset({'aborted', 'cancelled', 'canceled'}),
|
|
54
|
+
'error': frozenset({'error', 'failed'}),
|
|
55
|
+
'delta': frozenset({'delta', 'streaming', 'partial'}),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Subagent tool names — detected by pattern match, not exact strings
|
|
59
|
+
_SUBAGENT_SPAWN_PATTERNS = re.compile(
|
|
60
|
+
r'(sessions?[_-]spawn|spawn[_-]?sub[_-]?agent|sub[_-]?agent\.spawn|agent\.delegate)',
|
|
61
|
+
re.IGNORECASE
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
_SUBAGENT_SEND_PATTERNS = re.compile(
|
|
65
|
+
r'(agent[_-]send|agent\.send|delegate[_-]message)',
|
|
66
|
+
re.IGNORECASE
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Stale response model markers — gateway-injected responses to discard
|
|
70
|
+
_STALE_MODEL_MARKERS = frozenset({
|
|
71
|
+
'gateway-injected',
|
|
72
|
+
'system-generated',
|
|
73
|
+
'injected',
|
|
74
|
+
'replay',
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
# System response patterns to suppress (never surface to user)
|
|
78
|
+
_SYSTEM_RESPONSE_RE = re.compile(
|
|
79
|
+
r'^\s*(HEARTBEAT[_ ]?OK|heartbeat[_ ]?ok|ACK|PONG|system[_-]ok)\s*$',
|
|
80
|
+
re.IGNORECASE
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# Lookup helpers
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
def match_event(event_name: str) -> str | None:
|
|
89
|
+
"""Map an event name to its canonical form, or None if unknown.
|
|
90
|
+
|
|
91
|
+
>>> match_event('agent')
|
|
92
|
+
'agent'
|
|
93
|
+
>>> match_event('run')
|
|
94
|
+
'agent'
|
|
95
|
+
>>> match_event('unknown')
|
|
96
|
+
None
|
|
97
|
+
"""
|
|
98
|
+
for canonical, aliases in _EVENT_ALIASES.items():
|
|
99
|
+
if event_name in aliases:
|
|
100
|
+
return canonical
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def match_stream(stream_name: str) -> str | None:
|
|
105
|
+
"""Map a stream name to its canonical form."""
|
|
106
|
+
for canonical, aliases in _STREAM_ALIASES.items():
|
|
107
|
+
if stream_name in aliases:
|
|
108
|
+
return canonical
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def match_state(state_name: str) -> str | None:
|
|
113
|
+
"""Map a chat state to its canonical form."""
|
|
114
|
+
for canonical, aliases in _STATE_ALIASES.items():
|
|
115
|
+
if state_name in aliases:
|
|
116
|
+
return canonical
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def is_noise_event(event_name: str) -> bool:
|
|
121
|
+
"""Return True for events that should be silently dropped."""
|
|
122
|
+
return event_name in ('health', 'tick', 'presence', 'ping', 'keepalive')
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def is_subagent_spawn_tool(tool_name: str) -> bool:
|
|
126
|
+
"""Return True if this tool name indicates a subagent spawn."""
|
|
127
|
+
return bool(_SUBAGENT_SPAWN_PATTERNS.search(tool_name))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def is_subagent_send_tool(tool_name: str) -> bool:
|
|
131
|
+
"""Return True if this tool name indicates an agent-send."""
|
|
132
|
+
return bool(_SUBAGENT_SEND_PATTERNS.search(tool_name))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def is_subagent_tool(tool_name: str) -> bool:
|
|
136
|
+
"""Return True if this tool name is any subagent-related tool."""
|
|
137
|
+
return is_subagent_spawn_tool(tool_name) or is_subagent_send_tool(tool_name)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def is_stale_response(model: str, total_tokens: int) -> bool:
|
|
141
|
+
"""Return True if this looks like a gateway-injected stale replay."""
|
|
142
|
+
if model in _STALE_MODEL_MARKERS and total_tokens == 0:
|
|
143
|
+
return True
|
|
144
|
+
# Also check for explicit isStale flag (future OpenClaw versions)
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def is_stale_response_ex(model: str, total_tokens: int, payload: dict) -> bool:
|
|
149
|
+
"""Extended stale check — includes payload-level flags."""
|
|
150
|
+
if payload.get('isStale') or payload.get('injected'):
|
|
151
|
+
return True
|
|
152
|
+
return is_stale_response(model, total_tokens)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def is_system_response(text: str) -> bool:
|
|
156
|
+
"""Return True if this text is a system response that should be suppressed."""
|
|
157
|
+
return bool(_SYSTEM_RESPONSE_RE.match(text))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def is_subagent_session_key(session_key: str) -> bool:
|
|
161
|
+
"""Return True if this session key belongs to a subagent."""
|
|
162
|
+
# Accept multiple possible prefixes
|
|
163
|
+
return any(prefix in session_key for prefix in ('subagent:', 'sub:', 'child:'))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def extract_server_version(hello_result: dict) -> str | None:
|
|
167
|
+
"""Extract server version from hello response, trying multiple paths."""
|
|
168
|
+
if not hello_result:
|
|
169
|
+
return None
|
|
170
|
+
return (
|
|
171
|
+
hello_result.get('serverVersion')
|
|
172
|
+
or hello_result.get('version')
|
|
173
|
+
or (hello_result.get('server', {}) or {}).get('version')
|
|
174
|
+
or (hello_result.get('gateway', {}) or {}).get('version')
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def extract_run_id(data: dict) -> str | None:
|
|
179
|
+
"""Extract runId from an ACK response, trying multiple paths."""
|
|
180
|
+
result = data.get('result') or data.get('payload') or {}
|
|
181
|
+
return (
|
|
182
|
+
result.get('runId')
|
|
183
|
+
or data.get('runId')
|
|
184
|
+
or result.get('run_id')
|
|
185
|
+
or data.get('run_id')
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def extract_text_content(content) -> str:
|
|
190
|
+
"""Extract text from message content (string or Anthropic-style list).
|
|
191
|
+
|
|
192
|
+
Handles:
|
|
193
|
+
- Plain string
|
|
194
|
+
- List of {type: "text", text: "..."} blocks
|
|
195
|
+
- List of {type: "markdown", text: "..."} blocks
|
|
196
|
+
- Mixed content blocks (extracts all text-like entries)
|
|
197
|
+
"""
|
|
198
|
+
if isinstance(content, str):
|
|
199
|
+
return content
|
|
200
|
+
if isinstance(content, list):
|
|
201
|
+
text_parts = []
|
|
202
|
+
for item in content:
|
|
203
|
+
if isinstance(item, str):
|
|
204
|
+
text_parts.append(item)
|
|
205
|
+
elif isinstance(item, dict):
|
|
206
|
+
t = item.get('type', '')
|
|
207
|
+
text = item.get('text', '')
|
|
208
|
+
if t in ('text', 'markdown', 'plain') and text.strip():
|
|
209
|
+
text_parts.append(text)
|
|
210
|
+
elif not t and text.strip():
|
|
211
|
+
# No type field — assume text
|
|
212
|
+
text_parts.append(text)
|
|
213
|
+
return ' '.join(text_parts)
|
|
214
|
+
return ''
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def build_connect_params(
|
|
218
|
+
auth_token: str,
|
|
219
|
+
client_id: str = "cli",
|
|
220
|
+
client_mode: str = "cli",
|
|
221
|
+
platform: str = "linux",
|
|
222
|
+
user_agent: str = "openvoice-ui/1.0.0",
|
|
223
|
+
scopes: list | None = None,
|
|
224
|
+
caps: list | None = None,
|
|
225
|
+
device_block: dict | None = None,
|
|
226
|
+
) -> dict:
|
|
227
|
+
"""Build the params dict for a connect request.
|
|
228
|
+
|
|
229
|
+
Centralizes the handshake params shape so all callers stay in sync.
|
|
230
|
+
"""
|
|
231
|
+
if scopes is None:
|
|
232
|
+
scopes = ["operator.read", "operator.write"]
|
|
233
|
+
if caps is None:
|
|
234
|
+
caps = ["tool-events"]
|
|
235
|
+
|
|
236
|
+
params = {
|
|
237
|
+
"minProtocol": PROTOCOL_MIN,
|
|
238
|
+
"maxProtocol": PROTOCOL_MAX,
|
|
239
|
+
"client": {
|
|
240
|
+
"id": client_id,
|
|
241
|
+
"version": "1.0.0",
|
|
242
|
+
"platform": platform,
|
|
243
|
+
"mode": client_mode,
|
|
244
|
+
},
|
|
245
|
+
"role": "operator",
|
|
246
|
+
"scopes": scopes,
|
|
247
|
+
"caps": caps,
|
|
248
|
+
"commands": [],
|
|
249
|
+
"permissions": {},
|
|
250
|
+
"auth": {"token": auth_token},
|
|
251
|
+
"locale": "en-US",
|
|
252
|
+
"userAgent": user_agent,
|
|
253
|
+
}
|
|
254
|
+
if device_block:
|
|
255
|
+
params["device"] = device_block
|
|
256
|
+
return params
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def is_challenge_event(data: dict) -> bool:
|
|
260
|
+
"""Return True if this message is a connect challenge (current or future format)."""
|
|
261
|
+
if data.get('type') != 'event':
|
|
262
|
+
return False
|
|
263
|
+
evt = data.get('event', '')
|
|
264
|
+
return evt in _EVENT_ALIASES.get('connect.challenge', {evt})
|