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,337 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "openvoiceui/profile-schema",
|
|
4
|
+
"title": "Agent Profile",
|
|
5
|
+
"description": "Schema for agent personality and configuration profiles. ADR-002. v3.2 — full capability model: gateway queue, STT/TTS advanced, conversation flow, modes, session strategy, auth.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["id", "name", "system_prompt"],
|
|
8
|
+
"additionalProperties": true,
|
|
9
|
+
"properties": {
|
|
10
|
+
|
|
11
|
+
"id": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"pattern": "^[a-z0-9-]+$",
|
|
14
|
+
"description": "Unique identifier (lowercase, hyphens only)"
|
|
15
|
+
},
|
|
16
|
+
"name": { "type": "string", "maxLength": 50 },
|
|
17
|
+
"description": { "type": "string", "maxLength": 200 },
|
|
18
|
+
"version": { "type": "string", "pattern": "^\\d+\\.\\d+$", "default": "1.0" },
|
|
19
|
+
"author": { "type": "string" },
|
|
20
|
+
"created": { "type": "string", "format": "date" },
|
|
21
|
+
"icon": { "type": "string", "description": "Emoji icon for UI display" },
|
|
22
|
+
"mode": { "type": "string", "description": "Legacy special mode flag. Use adapter instead." },
|
|
23
|
+
"adapter": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"pattern": "^[a-z0-9-]+$",
|
|
26
|
+
"description": "Transport adapter: clawdbot | hume-evi | elevenlabs-classic. Defaults to clawdbot."
|
|
27
|
+
},
|
|
28
|
+
"adapter_config": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"additionalProperties": true,
|
|
31
|
+
"description": "Passed verbatim to adapter.init(). sessionKey + agentId are the clawdbot-specific keys."
|
|
32
|
+
},
|
|
33
|
+
"system_prompt": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"minLength": 10,
|
|
36
|
+
"description": "Agent personality / instructions."
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
"llm": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"required": ["provider"],
|
|
42
|
+
"additionalProperties": false,
|
|
43
|
+
"properties": {
|
|
44
|
+
"provider": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"enum": ["zai", "clawdbot", "gateway", "openai", "ollama", "anthropic", "hume", "elevenlabs"]
|
|
47
|
+
},
|
|
48
|
+
"model": { "type": "string" },
|
|
49
|
+
"config_id": { "type": "string" },
|
|
50
|
+
"parameters": {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"additionalProperties": false,
|
|
53
|
+
"properties": {
|
|
54
|
+
"max_tokens": { "type": "integer", "minimum": 1 },
|
|
55
|
+
"temperature": { "type": "number", "minimum": 0, "maximum": 2 }
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"queue": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"additionalProperties": false,
|
|
61
|
+
"description": "OpenClaw Gateway queue + streaming behaviour. null = use openclaw.json global default.",
|
|
62
|
+
"properties": {
|
|
63
|
+
"mode": {
|
|
64
|
+
"type": ["string", "null"],
|
|
65
|
+
"enum": ["collect", "steer", "steer-backlog", null],
|
|
66
|
+
"default": null,
|
|
67
|
+
"description": "collect = full response then TTS (safe, default). steer = user can interrupt mid-tool-chain, gateway pivots immediately. steer-backlog = steer but original message preserved."
|
|
68
|
+
},
|
|
69
|
+
"block_streaming_chunk": {
|
|
70
|
+
"type": ["integer", "null"],
|
|
71
|
+
"minimum": 10,
|
|
72
|
+
"maximum": 2000,
|
|
73
|
+
"default": null,
|
|
74
|
+
"description": "Min chars in streaming delta before firing a TTS sentence. Maps to _extract_sentence(min_len) in conversation.py. null = platform default (40)."
|
|
75
|
+
},
|
|
76
|
+
"block_streaming_coalesce": {
|
|
77
|
+
"type": ["boolean", "null"],
|
|
78
|
+
"default": null,
|
|
79
|
+
"description": "Coalesce streaming tokens into larger chunks before TTS. null = platform default."
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
"voice": {
|
|
87
|
+
"type": "object",
|
|
88
|
+
"required": ["tts_provider", "voice_id"],
|
|
89
|
+
"additionalProperties": false,
|
|
90
|
+
"properties": {
|
|
91
|
+
"tts_provider": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"enum": ["supertonic", "groq", "elevenlabs", "hume", "qwen3"]
|
|
94
|
+
},
|
|
95
|
+
"voice_id": { "type": "string" },
|
|
96
|
+
"speed": { "type": "number", "minimum": 0.5, "maximum": 2.0, "default": 1.0 },
|
|
97
|
+
"parameters":{ "type": "object" },
|
|
98
|
+
"notes": { "type": "string" },
|
|
99
|
+
"parallel_sentences": {
|
|
100
|
+
"type": ["boolean", "null"],
|
|
101
|
+
"default": null,
|
|
102
|
+
"description": "Fire TTS for all post-LLM sentences simultaneously. false = sequential. null = platform default (true). Set false if provider rate-limits parallel requests."
|
|
103
|
+
},
|
|
104
|
+
"min_sentence_chars": {
|
|
105
|
+
"type": ["integer", "null"],
|
|
106
|
+
"minimum": 5,
|
|
107
|
+
"maximum": 500,
|
|
108
|
+
"default": null,
|
|
109
|
+
"description": "Min chars before dispatching a chunk to TTS. Prevents TTS on single words. null = platform default (40)."
|
|
110
|
+
},
|
|
111
|
+
"inter_sentence_gap_ms": {
|
|
112
|
+
"type": ["integer", "null"],
|
|
113
|
+
"minimum": 0,
|
|
114
|
+
"maximum": 2000,
|
|
115
|
+
"default": null,
|
|
116
|
+
"description": "Silence between TTS audio chunks in playback. null = no gap. Future hook — not yet wired in frontend."
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
"stt": {
|
|
122
|
+
"type": "object",
|
|
123
|
+
"required": ["provider"],
|
|
124
|
+
"additionalProperties": false,
|
|
125
|
+
"properties": {
|
|
126
|
+
"provider": {
|
|
127
|
+
"type": "string",
|
|
128
|
+
"enum": ["deepgram", "deepgram-streaming", "deepgram-batch", "groq", "webspeech", "whisper", "hume", "elevenlabs"]
|
|
129
|
+
},
|
|
130
|
+
"language": { "type": "string", "default": "en-US" },
|
|
131
|
+
"notes": { "type": "string" },
|
|
132
|
+
"silence_timeout_ms": {
|
|
133
|
+
"type": ["integer", "null"],
|
|
134
|
+
"minimum": 500,
|
|
135
|
+
"maximum": 10000,
|
|
136
|
+
"default": null,
|
|
137
|
+
"description": "Ms of silence after final result before dispatching to AI. Maps to WebSpeechSTT.silenceDelayMs. null = platform default (3000ms)."
|
|
138
|
+
},
|
|
139
|
+
"vad_threshold": {
|
|
140
|
+
"type": ["integer", "null"],
|
|
141
|
+
"minimum": 10,
|
|
142
|
+
"maximum": 80,
|
|
143
|
+
"default": null,
|
|
144
|
+
"description": "FFT average amplitude threshold for voice detection. Lower = more sensitive. null = platform default (35). Range 10-80."
|
|
145
|
+
},
|
|
146
|
+
"max_recording_s": {
|
|
147
|
+
"type": ["integer", "null"],
|
|
148
|
+
"minimum": 10,
|
|
149
|
+
"maximum": 120,
|
|
150
|
+
"default": null,
|
|
151
|
+
"description": "Max recording duration in seconds before auto-chunking. Prevents FFmpeg timeout on long speech. null = platform default (45)."
|
|
152
|
+
},
|
|
153
|
+
"continuous": {
|
|
154
|
+
"type": ["boolean", "null"],
|
|
155
|
+
"default": null,
|
|
156
|
+
"description": "Continuous listening vs one-shot per utterance. null = provider default. false = PTT-only agents."
|
|
157
|
+
},
|
|
158
|
+
"wake_words": {
|
|
159
|
+
"type": ["array", "null"],
|
|
160
|
+
"items": { "type": "string" },
|
|
161
|
+
"default": null,
|
|
162
|
+
"description": "Override WakeWordDetector phrase list. null = platform defaults. [] = disable wake-word."
|
|
163
|
+
},
|
|
164
|
+
"wake_word_required": {
|
|
165
|
+
"type": ["boolean", "null"],
|
|
166
|
+
"default": null,
|
|
167
|
+
"description": "Start session in passive wake-word mode. null = not required (active from start)."
|
|
168
|
+
},
|
|
169
|
+
"ptt_default": {
|
|
170
|
+
"type": ["boolean", "null"],
|
|
171
|
+
"default": null,
|
|
172
|
+
"description": "Default UI to PTT mode on session start. null = continuous listening (current default)."
|
|
173
|
+
},
|
|
174
|
+
"identify_on_wake": {
|
|
175
|
+
"type": ["boolean", "null"],
|
|
176
|
+
"default": null,
|
|
177
|
+
"description": "Run face identification when wake word fires. Result is injected as context so agent can greet by name. null/true = enabled when camera is on."
|
|
178
|
+
},
|
|
179
|
+
"require_camera_auth": {
|
|
180
|
+
"type": ["boolean", "null"],
|
|
181
|
+
"default": null,
|
|
182
|
+
"description": "Block wake word activation unless a registered face is recognized (min 50% confidence). Requires camera to be on. null/false = disabled."
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
"context": {
|
|
188
|
+
"type": "object",
|
|
189
|
+
"additionalProperties": false,
|
|
190
|
+
"properties": {
|
|
191
|
+
"enable_fts": { "type": "boolean", "default": true },
|
|
192
|
+
"enable_briefing": { "type": "boolean", "default": true },
|
|
193
|
+
"enable_history": { "type": "boolean", "default": true },
|
|
194
|
+
"max_history_messages": { "type": "integer", "minimum": 1, "maximum": 100, "default": 12 },
|
|
195
|
+
"notes": { "type": "string" }
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
"vision": {
|
|
200
|
+
"type": "object",
|
|
201
|
+
"additionalProperties": false,
|
|
202
|
+
"description": "Vision / camera AI model configuration.",
|
|
203
|
+
"properties": {
|
|
204
|
+
"model": { "type": "string", "default": "glm-4.6v",
|
|
205
|
+
"description": "Vision model ID for camera analysis and face recognition." },
|
|
206
|
+
"provider": { "type": "string", "default": "zai",
|
|
207
|
+
"description": "Vision provider (zai = ZhipuAI/GLM)." }
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
"features": {
|
|
212
|
+
"type": "object",
|
|
213
|
+
"additionalProperties": true,
|
|
214
|
+
"description": "Feature toggles. Unknown features are allowed (forward-compat). Absent = off.",
|
|
215
|
+
"properties": {
|
|
216
|
+
"canvas": { "type": "boolean", "default": true },
|
|
217
|
+
"vision": { "type": "boolean", "default": true },
|
|
218
|
+
"music": { "type": "boolean", "default": false },
|
|
219
|
+
"tools": { "type": "boolean", "default": false },
|
|
220
|
+
"emotion_detection":{ "type": "boolean", "default": false },
|
|
221
|
+
"dj_soundboard": { "type": "boolean", "default": false }
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
"ui": {
|
|
226
|
+
"type": "object",
|
|
227
|
+
"additionalProperties": false,
|
|
228
|
+
"properties": {
|
|
229
|
+
"theme": { "type": "string", "enum": ["dark", "light"], "default": "dark" },
|
|
230
|
+
"theme_preset": { "type": "string", "enum": ["blue", "purple", "green", "red", "orange"] },
|
|
231
|
+
"face_enabled": { "type": "boolean", "default": true },
|
|
232
|
+
"face_mood": { "type": "string", "default": "neutral" },
|
|
233
|
+
"transcript_panel": { "type": "boolean", "default": true },
|
|
234
|
+
"thought_bubbles": { "type": "boolean", "default": true },
|
|
235
|
+
"show_mode_badge": { "type": "boolean", "default": false },
|
|
236
|
+
"mode_badge_text": { "type": "string" }
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
"speech_normalization": {
|
|
241
|
+
"type": "object",
|
|
242
|
+
"additionalProperties": false,
|
|
243
|
+
"properties": {
|
|
244
|
+
"strip_markdown": { "type": "boolean", "default": true },
|
|
245
|
+
"strip_urls": { "type": "boolean", "default": true },
|
|
246
|
+
"strip_emoji": { "type": "boolean", "default": true },
|
|
247
|
+
"max_length": { "type": "integer", "minimum": 50, "maximum": 5000, "default": 800 },
|
|
248
|
+
"abbreviations": { "type": "object", "additionalProperties": { "type": "string" } }
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
"conversation": {
|
|
253
|
+
"type": "object",
|
|
254
|
+
"additionalProperties": false,
|
|
255
|
+
"description": "Conversation flow controls.",
|
|
256
|
+
"properties": {
|
|
257
|
+
"greeting": {
|
|
258
|
+
"type": ["string", "null"],
|
|
259
|
+
"maxLength": 500,
|
|
260
|
+
"default": null,
|
|
261
|
+
"description": "Text spoken on session start. null = random pool. '' = silent connect."
|
|
262
|
+
},
|
|
263
|
+
"auto_hangup_silence_ms": {
|
|
264
|
+
"type": ["integer", "null"],
|
|
265
|
+
"minimum": 5000,
|
|
266
|
+
"maximum": 600000,
|
|
267
|
+
"default": null,
|
|
268
|
+
"description": "Auto-hangup after N ms total silence. null = never."
|
|
269
|
+
},
|
|
270
|
+
"interruption_enabled": {
|
|
271
|
+
"type": ["boolean", "null"],
|
|
272
|
+
"default": null,
|
|
273
|
+
"description": "STT active during TTS playback — user can barge in. Requires llm.queue.mode=steer on gateway. null = platform default (false, STT blocked during TTS)."
|
|
274
|
+
},
|
|
275
|
+
"max_response_chars": {
|
|
276
|
+
"type": ["integer", "null"],
|
|
277
|
+
"minimum": 50,
|
|
278
|
+
"maximum": 16000,
|
|
279
|
+
"default": null,
|
|
280
|
+
"description": "Hard cap on AI response before TTS. Truncates at sentence boundary. null = no cap."
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
"modes": {
|
|
286
|
+
"type": "object",
|
|
287
|
+
"additionalProperties": false,
|
|
288
|
+
"description": "Which UI mode buttons are available for this agent.",
|
|
289
|
+
"properties": {
|
|
290
|
+
"normal": { "type": "boolean", "default": true, "description": "Standard continuous voice." },
|
|
291
|
+
"listen": { "type": "boolean", "default": false, "description": "Transcribe-only, no AI sends." },
|
|
292
|
+
"ptt": { "type": "boolean", "default": true, "description": "Push-to-talk UI option." },
|
|
293
|
+
"a2a": { "type": "boolean", "default": false, "description": "Agent-to-Agent programmatic input mode." }
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
"session": {
|
|
298
|
+
"type": "object",
|
|
299
|
+
"additionalProperties": false,
|
|
300
|
+
"description": "Gateway session key strategy.",
|
|
301
|
+
"properties": {
|
|
302
|
+
"key_strategy": {
|
|
303
|
+
"type": ["string", "null"],
|
|
304
|
+
"enum": ["persistent", "per-call", "per-message", null],
|
|
305
|
+
"default": null,
|
|
306
|
+
"description": "persistent = warm shared key (best for voice). per-call = new key per session.start(). per-message = fresh key per message (stateless). null = defer to adapter_config.sessionKey or platform default."
|
|
307
|
+
},
|
|
308
|
+
"key_prefix": {
|
|
309
|
+
"type": ["string", "null"],
|
|
310
|
+
"pattern": "^[a-z0-9-]+$",
|
|
311
|
+
"default": null,
|
|
312
|
+
"description": "Prefix for auto-generated keys. 'voice-dj' → 'voice-dj-1'. null = use env var or 'voice-main'."
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
"auth": {
|
|
318
|
+
"type": "object",
|
|
319
|
+
"additionalProperties": false,
|
|
320
|
+
"description": "Per-profile auth override. Supplements global Clerk auth gate.",
|
|
321
|
+
"properties": {
|
|
322
|
+
"required": {
|
|
323
|
+
"type": ["boolean", "null"],
|
|
324
|
+
"default": null,
|
|
325
|
+
"description": "Override global auth. true = always require Clerk. false = public even when global on. null = inherit global."
|
|
326
|
+
},
|
|
327
|
+
"allowed_roles": {
|
|
328
|
+
"type": ["array", "null"],
|
|
329
|
+
"items": { "type": "string" },
|
|
330
|
+
"default": null,
|
|
331
|
+
"description": "Clerk roles allowed to activate this profile. null = any auth user. [] = disabled. ['admin'] = admin only."
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
}
|
|
337
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# OpenVoiceUI — Voice System Prompt
|
|
2
|
+
# ============================================================
|
|
3
|
+
# This file is injected before every user message sent to the LLM gateway.
|
|
4
|
+
# It is the SINGLE SOURCE OF TRUTH for all interface capabilities.
|
|
5
|
+
# Lines starting with # are comments — stripped before sending. NOT seen by the LLM.
|
|
6
|
+
#
|
|
7
|
+
# HOT-RELOAD: Changes here take effect on the next conversation request. No restart needed.
|
|
8
|
+
# EDIT VIA ADMIN API: PUT /api/instructions/voice-system-prompt
|
|
9
|
+
#
|
|
10
|
+
# SCOPE: Everything in this file is OpenVoiceUI-native — independent of OpenClaw
|
|
11
|
+
# workspace files, agent personality, or any external configuration.
|
|
12
|
+
# An agent with completely empty workspace files can operate the full interface
|
|
13
|
+
# using ONLY what is documented here.
|
|
14
|
+
#
|
|
15
|
+
# WORKSPACE FILES (AGENTS.md, SOUL.md, TOOLS.md, etc.) are for PERSONALIZATION ONLY:
|
|
16
|
+
# - Custom behavior on specific canvas pages ("speak like a bartender on the tavern page")
|
|
17
|
+
# - Auto-actions on specific user patterns ("when I open playlist, start playing")
|
|
18
|
+
# - Business context, user preferences, agent identity
|
|
19
|
+
# ============================================================
|
|
20
|
+
|
|
21
|
+
[OPENVOICEUI SYSTEM INSTRUCTIONS:
|
|
22
|
+
|
|
23
|
+
VOICE AND TONE:
|
|
24
|
+
You are a voice AI assistant embedded in OpenVoiceUI. Always respond in English.
|
|
25
|
+
Respond in natural, conversational tone — NO markdown (no #, -, *, bullet lists, or tables).
|
|
26
|
+
Be brief and direct. Use paragraphs, not lists. Never sound like a call center agent.
|
|
27
|
+
BANNED OPENERS — never start a response with: "Hey there", "Great question", "Absolutely", "Of course", "Certainly", "Sure thing", "I hear you", "I understand you saying", "That's a great", or any variation. Just answer directly.
|
|
28
|
+
Do NOT repeat or paraphrase what the user just said. Do NOT end every reply with a question.
|
|
29
|
+
|
|
30
|
+
IDENTITY:
|
|
31
|
+
Do NOT address anyone by name unless a [FACE RECOGNITION] tag appears in this exact message confirming their identity. Different people use this interface. Never use names from memory or prior sessions without face recognition confirmation in the current message.
|
|
32
|
+
When a [FACE RECOGNITION] tag IS present, greet the person naturally by name and speak to them personally for the rest of the session.
|
|
33
|
+
|
|
34
|
+
CRITICAL RULE — WORDS WITH EVERY TAG:
|
|
35
|
+
Every response MUST contain spoken words alongside any action tags. NEVER output a bare tag alone — the user hears silence and sees nothing.
|
|
36
|
+
BAD: [CANVAS:page-id] GOOD: Here is your dashboard. [CANVAS:page-id]
|
|
37
|
+
BAD: [MUSIC_PLAY] GOOD: Playing something for you now. [MUSIC_PLAY]
|
|
38
|
+
Tags are invisible to the user — they only hear your words and see your words.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
CANVAS — OPEN EXISTING PAGE:
|
|
43
|
+
[CANVAS:page-id] opens a canvas page in the UI overlay. Use the exact page-id from the [Canvas pages:] list provided above in this message. When opening, briefly say what the page shows (1-2 sentences).
|
|
44
|
+
[CANVAS_MENU] opens the page picker so the user can browse all available pages.
|
|
45
|
+
[CANVAS_URL:https://example.com] loads an external URL inside the canvas iframe (only works on sites that allow iframe embedding).
|
|
46
|
+
CRITICAL: NEVER use the openclaw "canvas" tool with action:"present" — it fails with "node required". ONLY the [CANVAS:page-id] tag works to open pages.
|
|
47
|
+
Repeating [CANVAS:same-page] on an already-open page forces a refresh — use this after updating a page.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
CANVAS — CREATE A NEW PAGE:
|
|
52
|
+
Step 1 — Write the HTML file using your write tool: path is workspace/canvas/pagename.html
|
|
53
|
+
Step 2 — Open it in your response: say something like "Here it is. [CANVAS:pagename]"
|
|
54
|
+
Step 3 — Verify it opened: exec("curl -s http://openvoiceui:5001/api/canvas/context") returns {"current_page": "pagename.html", "current_title": "..."}
|
|
55
|
+
If current_page matches what you opened — confirm to user: "You should be seeing [page name] now."
|
|
56
|
+
If current_page is still the old page — say so and resend [CANVAS:pagename].
|
|
57
|
+
If current_page is null or empty — say "Opening the canvas now." and resend [CANVAS:pagename].
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
CANVAS — HTML RULES (mandatory for every page you create):
|
|
62
|
+
NO external CDN scripts — Tailwind CDN, Bootstrap CDN, any <script src="https://..."> are BANNED. They silently break inside sandboxed iframes.
|
|
63
|
+
All CSS and JS must be inline — inside <style> and <script> tags only.
|
|
64
|
+
Google Fonts @import url(...) inside a <style> tag is OK (graceful fallback if it fails).
|
|
65
|
+
Dark theme: background #0d1117 or #13141a, text #e2e8f0, accent blue #3b82f6 or amber #f59e0b.
|
|
66
|
+
Body CSS must include: padding: 20px; color: #e2e8f0; background: #0a0a0a;
|
|
67
|
+
Make pages visual — use cards, grids, tables, icons, real data from the conversation. No blank pages.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
CANVAS — INTERACTIVE BUTTONS:
|
|
72
|
+
Use postMessage for buttons that trigger AI actions — NEVER use href="#" (does nothing in iframe).
|
|
73
|
+
Send text to AI: onclick="window.parent.postMessage({type:'canvas-action', action:'speak', text:'your message here'}, '*')"
|
|
74
|
+
Open another page: onclick="window.parent.postMessage({type:'canvas-action', action:'navigate', page:'page-id'}, '*')"
|
|
75
|
+
Open page picker menu: onclick="window.parent.postMessage({type:'canvas-action', action:'menu'}, '*')"
|
|
76
|
+
Close canvas: onclick="window.parent.postMessage({type:'canvas-action', action:'close'}, '*')"
|
|
77
|
+
External links that open new tab: <a href="https://example.com" target="_blank">Link text</a>
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
CANVAS — MAKE A PAGE PUBLIC (shareable without login):
|
|
82
|
+
exec("curl -s -X PATCH http://openvoiceui:5001/api/canvas/manifest/page/PAGE_ID -H 'Content-Type: application/json' -d '{\"is_public\": true}'")
|
|
83
|
+
Replace PAGE_ID with the page filename without .html extension.
|
|
84
|
+
To make private again: use {"is_public": false}
|
|
85
|
+
Shareable URL format: https://DOMAIN/pages/pagename.html
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
MUSIC CONTROL:
|
|
90
|
+
[MUSIC_PLAY] plays a random track from the library.
|
|
91
|
+
[MUSIC_PLAY:track name] plays a specific track — use the exact title from the [Available tracks:] list provided above in this message.
|
|
92
|
+
[MUSIC_STOP] stops music playback.
|
|
93
|
+
[MUSIC_NEXT] skips to the next track.
|
|
94
|
+
Only use music tags when the user explicitly asks — EXCEPT when opening a music-related canvas page (music-list, playlist, library, etc.), also send [MUSIC_PLAY] in the same response so music starts automatically alongside the page.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
SONG GENERATION (AI Music via Suno):
|
|
99
|
+
[SUNO_GENERATE:description of the song] generates a new AI song. Takes approximately 45 seconds.
|
|
100
|
+
Always tell the user what to expect: say something like "I will get that cooking now — should be ready in about 45 seconds!"
|
|
101
|
+
The frontend handles the Suno API and shows a notification when done. Do NOT call any Suno APIs yourself.
|
|
102
|
+
After generation, the new song appears in the [Available tracks:] list by its title. Use [MUSIC_PLAY:song title] to play it — do NOT use exec or shell commands to search for the file. The music system matches by title automatically.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
SPOTIFY:
|
|
107
|
+
[SPOTIFY:song name] or [SPOTIFY:song name|artist name] switches the player to Spotify and plays that track.
|
|
108
|
+
Example: [SPOTIFY:Bohemian Rhapsody|Queen]
|
|
109
|
+
Only use when the user specifically asks for a Spotify track.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
SLEEP — GOODBYE AND DEACTIVATE:
|
|
114
|
+
[SLEEP] puts the interface into passive wake-word listening mode.
|
|
115
|
+
Use when the user says goodbye, goodnight, stop listening, go to sleep, I am out, peace, later, or any farewell phrase.
|
|
116
|
+
Always give a brief farewell (1-2 sentences) BEFORE the [SLEEP] tag.
|
|
117
|
+
Examples: "Later! Catch you next time. [SLEEP]" or "Goodnight! Sweet dreams. [SLEEP]"
|
|
118
|
+
NEVER say you "should" go to sleep without including the [SLEEP] tag — the tag IS the action. Saying it without the tag does nothing.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
SESSION RESET:
|
|
123
|
+
[SESSION_RESET] clears the conversation history and starts fresh.
|
|
124
|
+
Use sparingly — only when the context is clearly broken or the user explicitly asks to start over.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
DJ SOUNDBOARD:
|
|
129
|
+
[SOUND:name] plays a sound effect.
|
|
130
|
+
ONLY use in DJ mode — triggered when the user explicitly says "be a DJ", "DJ mode", or "put on a set".
|
|
131
|
+
NEVER use sound tags in normal conversation.
|
|
132
|
+
Available sounds: air_horn, scratch_long, rewind, record_stop, crowd_cheer, crowd_hype, yeah, lets_go, gunshot, bruh, sad_trombone
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
FACE REGISTRATION:
|
|
137
|
+
[REGISTER_FACE:Name] captures and saves the person's face from the camera.
|
|
138
|
+
Only use when someone explicitly asks or introduces themselves — never register without consent.
|
|
139
|
+
If the camera is off, tell the user they need to turn it on first.
|
|
140
|
+
Example: "Nice to meet you! I will remember your face. [REGISTER_FACE:Sarah]"
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
CAMERA VISION:
|
|
145
|
+
When a [CAMERA VISION: ...] tag appears in the context above this message, it contains a description of what the camera currently sees, analyzed by a vision model.
|
|
146
|
+
Use this to answer the user's question naturally in your own words — do not repeat the raw description verbatim.
|
|
147
|
+
If the vision tag says the camera is off or unavailable, tell the user they need to turn on the camera first.
|
|
148
|
+
|
|
149
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provider package — ADR-003: Abstract base class + registry pattern.
|
|
3
|
+
|
|
4
|
+
Sub-packages:
|
|
5
|
+
providers.llm — LLMProvider base class + concrete providers
|
|
6
|
+
providers.tts — TTSProvider base class + concrete providers
|
|
7
|
+
providers.stt — STTProvider base class + concrete providers
|
|
8
|
+
providers.registry — ProviderRegistry singleton (P5-T2)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from providers.base import (
|
|
12
|
+
BaseProvider,
|
|
13
|
+
ProviderError,
|
|
14
|
+
ProviderUnavailableError,
|
|
15
|
+
ProviderGenerationError,
|
|
16
|
+
)
|
|
17
|
+
from providers.registry import (
|
|
18
|
+
ProviderRegistry,
|
|
19
|
+
ProviderType,
|
|
20
|
+
registry,
|
|
21
|
+
get_llm_provider,
|
|
22
|
+
get_tts_provider,
|
|
23
|
+
get_stt_provider,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
# Base classes
|
|
28
|
+
"BaseProvider",
|
|
29
|
+
"ProviderError",
|
|
30
|
+
"ProviderUnavailableError",
|
|
31
|
+
"ProviderGenerationError",
|
|
32
|
+
# Registry
|
|
33
|
+
"ProviderRegistry",
|
|
34
|
+
"ProviderType",
|
|
35
|
+
"registry",
|
|
36
|
+
"get_llm_provider",
|
|
37
|
+
"get_tts_provider",
|
|
38
|
+
"get_stt_provider",
|
|
39
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provider abstract base classes — ADR-003: Abstract base class + registry pattern.
|
|
3
|
+
|
|
4
|
+
All provider types (LLM, TTS, STT) share this common interface contract.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from typing import Any, Dict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseProvider(ABC):
|
|
12
|
+
"""Common base for all provider types."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, config: Dict[str, Any] = None):
|
|
15
|
+
self._config = config or {}
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def is_available(self) -> bool:
|
|
19
|
+
"""Return True if the provider can handle requests right now."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def get_info(self) -> Dict[str, Any]:
|
|
24
|
+
"""Return metadata dict with at minimum 'name' and 'status' keys."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
def get_config(self, key: str, default: Any = None) -> Any:
|
|
28
|
+
"""Safely read a value from the provider config dict."""
|
|
29
|
+
return self._config.get(key, default)
|
|
30
|
+
|
|
31
|
+
def __repr__(self) -> str:
|
|
32
|
+
info = self.get_info()
|
|
33
|
+
return (
|
|
34
|
+
f"{self.__class__.__name__}("
|
|
35
|
+
f"name='{info.get('name', 'unknown')}', "
|
|
36
|
+
f"available={self.is_available()})"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ProviderError(Exception):
|
|
41
|
+
"""Base exception for all provider errors."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, provider_name: str, message: str):
|
|
44
|
+
self.provider_name = provider_name
|
|
45
|
+
super().__init__(f"[{provider_name}] {message}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ProviderUnavailableError(ProviderError):
|
|
49
|
+
"""Raised when a provider is not available or not configured."""
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ProviderGenerationError(ProviderError):
|
|
54
|
+
"""Raised when a provider fails during generation/inference."""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
"BaseProvider",
|
|
60
|
+
"ProviderError",
|
|
61
|
+
"ProviderUnavailableError",
|
|
62
|
+
"ProviderGenerationError",
|
|
63
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""LLM provider package.
|
|
2
|
+
|
|
3
|
+
Importing this package registers all LLM providers with the registry.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from providers.llm.base import LLMProvider, LLMResponse, LLMError
|
|
7
|
+
|
|
8
|
+
# Import concrete providers so their registry.register() calls fire
|
|
9
|
+
from providers.llm import zai_provider # noqa: F401
|
|
10
|
+
from providers.llm import clawdbot_provider # noqa: F401
|
|
11
|
+
|
|
12
|
+
__all__ = ["LLMProvider", "LLMResponse", "LLMError"]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM provider abstract base class.
|
|
3
|
+
|
|
4
|
+
Based on: future-dev-plans/02-PROVIDER-SYSTEMS.md (llm_providers/base.py section)
|
|
5
|
+
ADR-003: Abstract base class + registry pattern.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import abstractmethod
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any, Dict, Iterator, List, Optional
|
|
11
|
+
|
|
12
|
+
from providers.base import BaseProvider, ProviderError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class LLMResponse:
|
|
17
|
+
content: str
|
|
18
|
+
model: str
|
|
19
|
+
provider: str
|
|
20
|
+
usage: Dict[str, int] = field(default_factory=dict)
|
|
21
|
+
latency_ms: float = 0.0
|
|
22
|
+
finish_reason: str = "stop"
|
|
23
|
+
raw_response: Any = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LLMProvider(BaseProvider):
|
|
27
|
+
"""Abstract base class for LLM providers (ZAI, Clawdbot, OpenAI, etc.)."""
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def generate(
|
|
31
|
+
self,
|
|
32
|
+
messages: List[Dict[str, str]],
|
|
33
|
+
system_prompt: Optional[str] = None,
|
|
34
|
+
model: Optional[str] = None,
|
|
35
|
+
**kwargs,
|
|
36
|
+
) -> LLMResponse:
|
|
37
|
+
"""Generate a complete (non-streaming) response."""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def generate_stream(
|
|
42
|
+
self,
|
|
43
|
+
messages: List[Dict[str, str]],
|
|
44
|
+
system_prompt: Optional[str] = None,
|
|
45
|
+
model: Optional[str] = None,
|
|
46
|
+
**kwargs,
|
|
47
|
+
) -> Iterator[str]:
|
|
48
|
+
"""Generate a streaming response, yielding text chunks."""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
def list_models(self) -> List[Dict[str, Any]]:
|
|
52
|
+
return self._config.get("models", [])
|
|
53
|
+
|
|
54
|
+
def get_default_model(self) -> str:
|
|
55
|
+
return self._config.get("default_model", "default")
|
|
56
|
+
|
|
57
|
+
def get_info(self) -> Dict[str, Any]:
|
|
58
|
+
return {
|
|
59
|
+
"name": self._config.get("name", self.__class__.__name__),
|
|
60
|
+
"models": self.list_models(),
|
|
61
|
+
"available": self.is_available(),
|
|
62
|
+
"status": "active" if self.is_available() else "inactive",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class LLMError(ProviderError):
|
|
67
|
+
"""LLM-specific provider error."""
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
__all__ = ["LLMProvider", "LLMResponse", "LLMError"]
|