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.
Files changed (185) hide show
  1. package/.env.example +104 -0
  2. package/Dockerfile +30 -0
  3. package/LICENSE +21 -0
  4. package/README.md +638 -0
  5. package/SETUP.md +360 -0
  6. package/app.py +232 -0
  7. package/auto-approve-devices.js +111 -0
  8. package/cli/index.js +372 -0
  9. package/config/__init__.py +4 -0
  10. package/config/default.yaml +43 -0
  11. package/config/flags.yaml +67 -0
  12. package/config/loader.py +203 -0
  13. package/config/providers.yaml +71 -0
  14. package/config/speech_normalization.yaml +182 -0
  15. package/config/theme.json +4 -0
  16. package/data/greetings.json +25 -0
  17. package/default-pages/ai-image-creator.html +915 -0
  18. package/default-pages/bulk-image-uploader.html +492 -0
  19. package/default-pages/desktop.html +2865 -0
  20. package/default-pages/file-explorer.html +854 -0
  21. package/default-pages/interactive-map.html +655 -0
  22. package/default-pages/style-guide.html +1005 -0
  23. package/default-pages/website-setup.html +1623 -0
  24. package/deploy/openclaw/Dockerfile +46 -0
  25. package/deploy/openvoiceui.service +30 -0
  26. package/deploy/setup-nginx.sh +50 -0
  27. package/deploy/setup-sudo.sh +306 -0
  28. package/deploy/skill-runner/Dockerfile +19 -0
  29. package/deploy/skill-runner/requirements.txt +14 -0
  30. package/deploy/skill-runner/server.py +269 -0
  31. package/deploy/supertonic/Dockerfile +22 -0
  32. package/deploy/supertonic/server.py +79 -0
  33. package/docker-compose.pinokio.yml +11 -0
  34. package/docker-compose.yml +59 -0
  35. package/greetings.json +25 -0
  36. package/index.html +65 -0
  37. package/inject-device-identity.js +142 -0
  38. package/package.json +82 -0
  39. package/profiles/default.json +114 -0
  40. package/profiles/manager.py +354 -0
  41. package/profiles/schema.json +337 -0
  42. package/prompts/voice-system-prompt.md +149 -0
  43. package/providers/__init__.py +39 -0
  44. package/providers/base.py +63 -0
  45. package/providers/llm/__init__.py +12 -0
  46. package/providers/llm/base.py +71 -0
  47. package/providers/llm/clawdbot_provider.py +112 -0
  48. package/providers/llm/zai_provider.py +115 -0
  49. package/providers/registry.py +320 -0
  50. package/providers/stt/__init__.py +12 -0
  51. package/providers/stt/base.py +58 -0
  52. package/providers/stt/webspeech_provider.py +49 -0
  53. package/providers/stt/whisper_provider.py +100 -0
  54. package/providers/tts/__init__.py +20 -0
  55. package/providers/tts/base.py +91 -0
  56. package/providers/tts/groq_provider.py +74 -0
  57. package/providers/tts/supertonic_provider.py +72 -0
  58. package/requirements.txt +38 -0
  59. package/routes/__init__.py +10 -0
  60. package/routes/admin.py +515 -0
  61. package/routes/canvas.py +1315 -0
  62. package/routes/chat.py +51 -0
  63. package/routes/conversation.py +2158 -0
  64. package/routes/elevenlabs_hybrid.py +306 -0
  65. package/routes/greetings.py +98 -0
  66. package/routes/icons.py +279 -0
  67. package/routes/image_gen.py +364 -0
  68. package/routes/instructions.py +190 -0
  69. package/routes/music.py +838 -0
  70. package/routes/onboarding.py +43 -0
  71. package/routes/pi.py +62 -0
  72. package/routes/profiles.py +215 -0
  73. package/routes/report_issue.py +68 -0
  74. package/routes/static_files.py +533 -0
  75. package/routes/suno.py +664 -0
  76. package/routes/theme.py +81 -0
  77. package/routes/transcripts.py +199 -0
  78. package/routes/vision.py +348 -0
  79. package/routes/workspace.py +288 -0
  80. package/server.py +1510 -0
  81. package/services/__init__.py +1 -0
  82. package/services/auth.py +143 -0
  83. package/services/canvas_versioning.py +239 -0
  84. package/services/db_pool.py +107 -0
  85. package/services/gateway.py +16 -0
  86. package/services/gateway_manager.py +333 -0
  87. package/services/gateways/__init__.py +12 -0
  88. package/services/gateways/base.py +110 -0
  89. package/services/gateways/compat.py +264 -0
  90. package/services/gateways/openclaw.py +1134 -0
  91. package/services/health.py +100 -0
  92. package/services/memory_client.py +455 -0
  93. package/services/paths.py +26 -0
  94. package/services/speech_normalizer.py +285 -0
  95. package/services/tts.py +270 -0
  96. package/setup-config.js +262 -0
  97. package/sounds/air_horn.mp3 +0 -0
  98. package/sounds/bruh.mp3 +0 -0
  99. package/sounds/crowd_cheer.mp3 +0 -0
  100. package/sounds/gunshot.mp3 +0 -0
  101. package/sounds/impact.mp3 +0 -0
  102. package/sounds/lets_go.mp3 +0 -0
  103. package/sounds/record_stop.mp3 +0 -0
  104. package/sounds/rewind.mp3 +0 -0
  105. package/sounds/sad_trombone.mp3 +0 -0
  106. package/sounds/scratch_long.mp3 +0 -0
  107. package/sounds/yeah.mp3 +0 -0
  108. package/src/adapters/ClawdBotAdapter.js +264 -0
  109. package/src/adapters/_template.js +133 -0
  110. package/src/adapters/elevenlabs-classic.js +841 -0
  111. package/src/adapters/elevenlabs-hybrid.js +812 -0
  112. package/src/adapters/hume-evi.js +676 -0
  113. package/src/admin.html +1339 -0
  114. package/src/app.js +8802 -0
  115. package/src/core/Config.js +173 -0
  116. package/src/core/EmotionEngine.js +307 -0
  117. package/src/core/EventBridge.js +180 -0
  118. package/src/core/EventBus.js +117 -0
  119. package/src/core/VoiceSession.js +607 -0
  120. package/src/face/BaseFace.js +259 -0
  121. package/src/face/EyeFace.js +208 -0
  122. package/src/face/HaloSmokeFace.js +509 -0
  123. package/src/face/manifest.json +27 -0
  124. package/src/face/previews/eyes.svg +16 -0
  125. package/src/face/previews/orb.svg +29 -0
  126. package/src/features/MusicPlayer.js +620 -0
  127. package/src/features/Soundboard.js +128 -0
  128. package/src/providers/DeepgramSTT.js +472 -0
  129. package/src/providers/DeepgramStreamingSTT.js +766 -0
  130. package/src/providers/GroqSTT.js +559 -0
  131. package/src/providers/TTSPlayer.js +323 -0
  132. package/src/providers/WebSpeechSTT.js +479 -0
  133. package/src/providers/tts/BaseTTSProvider.js +81 -0
  134. package/src/providers/tts/HumeProvider.js +77 -0
  135. package/src/providers/tts/SupertonicProvider.js +174 -0
  136. package/src/providers/tts/index.js +140 -0
  137. package/src/shell/adapter-registry.js +154 -0
  138. package/src/shell/caller-bridge.js +35 -0
  139. package/src/shell/camera-bridge.js +28 -0
  140. package/src/shell/canvas-bridge.js +32 -0
  141. package/src/shell/commercial-bridge.js +44 -0
  142. package/src/shell/face-bridge.js +44 -0
  143. package/src/shell/music-bridge.js +60 -0
  144. package/src/shell/orchestrator.js +233 -0
  145. package/src/shell/profile-discovery.js +303 -0
  146. package/src/shell/sounds-bridge.js +28 -0
  147. package/src/shell/transcript-bridge.js +61 -0
  148. package/src/shell/waveform-bridge.js +33 -0
  149. package/src/styles/base.css +2862 -0
  150. package/src/styles/face.css +417 -0
  151. package/src/styles/pi-overrides.css +89 -0
  152. package/src/styles/theme-dark.css +67 -0
  153. package/src/test-tts.html +175 -0
  154. package/src/ui/AppShell.js +544 -0
  155. package/src/ui/ProfileSwitcher.js +228 -0
  156. package/src/ui/SessionControl.js +240 -0
  157. package/src/ui/face/FacePicker.js +195 -0
  158. package/src/ui/face/FaceRenderer.js +309 -0
  159. package/src/ui/settings/PlaylistEditor.js +366 -0
  160. package/src/ui/settings/SettingsPanel.css +684 -0
  161. package/src/ui/settings/SettingsPanel.js +419 -0
  162. package/src/ui/settings/TTSVoicePreview.js +210 -0
  163. package/src/ui/themes/ThemeManager.js +213 -0
  164. package/src/ui/visualizers/BaseVisualizer.js +29 -0
  165. package/src/ui/visualizers/PartyFXVisualizer.css +291 -0
  166. package/src/ui/visualizers/PartyFXVisualizer.js +637 -0
  167. package/static/emulators/jsdos/js-dos.css +1 -0
  168. package/static/emulators/jsdos/js-dos.js +22 -0
  169. package/static/favicon.svg +55 -0
  170. package/static/icons/apple-touch-icon.png +0 -0
  171. package/static/icons/favicon-32.png +0 -0
  172. package/static/icons/icon-192.png +0 -0
  173. package/static/icons/icon-512.png +0 -0
  174. package/static/install.html +449 -0
  175. package/static/manifest.json +26 -0
  176. package/static/sw.js +21 -0
  177. package/tts_providers/__init__.py +136 -0
  178. package/tts_providers/base_provider.py +319 -0
  179. package/tts_providers/groq_provider.py +155 -0
  180. package/tts_providers/hume_provider.py +226 -0
  181. package/tts_providers/providers_config.json +119 -0
  182. package/tts_providers/qwen3_provider.py +371 -0
  183. package/tts_providers/resemble_provider.py +315 -0
  184. package/tts_providers/supertonic_provider.py +557 -0
  185. package/tts_providers/supertonic_tts.py +399 -0
@@ -0,0 +1,323 @@
1
+ /**
2
+ * TTSPlayer — Frontend TTS audio playback with waveform analysis
3
+ *
4
+ * Extracted from index.html (VoiceConversation.playTTS, ClawbotMode.playAudio,
5
+ * startAnalyserAnimation, stopAnalyserAnimation, base64ToArrayBuffer/Blob helpers).
6
+ *
7
+ * Usage:
8
+ * import { TTSPlayer } from './providers/TTSPlayer.js';
9
+ * const player = new TTSPlayer();
10
+ * await player.init();
11
+ * player.onAmplitude = (value) => waveformModule.setAmplitude(value);
12
+ * player.onSpeakingChange = (isSpeaking) => { ... };
13
+ * await player.play(base64Audio); // AudioContext path
14
+ * player.queue(base64Audio); // Queue path (ClawbotMode style)
15
+ * player.stop();
16
+ */
17
+ export class TTSPlayer {
18
+ constructor() {
19
+ // AudioContext-based path (used by VoiceConversation / direct TTS)
20
+ this.audioContext = null;
21
+ this.gainNode = null;
22
+ this.analyser = null;
23
+ this.analyserData = null;
24
+ this.analyserAnimationId = null;
25
+ this.currentSource = null; // BufferSourceNode
26
+
27
+ // Queue-based path (used by ClawbotMode for streamed chunks)
28
+ this.audioQueue = [];
29
+ this.currentAudio = null; // HTMLAudioElement
30
+ this.isPlaying = false;
31
+
32
+ // Volume boost — iOS Safari web audio is quieter than native apps.
33
+ // GainNode values > 1.0 amplify the signal. Default 1.5x on mobile, 1.0x desktop.
34
+ this._isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
35
+ this.gain = this._isMobile ? 1.8 : 1.0;
36
+
37
+ // Callbacks
38
+ this.onAmplitude = null; // (value: 0-1) => void
39
+ this.onSpeakingChange = null; // (isSpeaking: boolean) => void
40
+ }
41
+
42
+ /**
43
+ * Initialize AudioContext and analyser.
44
+ * Must be called after a user gesture on some browsers.
45
+ */
46
+ async init() {
47
+ if (this.audioContext) return;
48
+
49
+ this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
50
+
51
+ // GainNode for volume boost (especially on mobile where web audio is quieter)
52
+ this.gainNode = this.audioContext.createGain();
53
+ this.gainNode.gain.value = this.gain;
54
+
55
+ this.analyser = this.audioContext.createAnalyser();
56
+ this.analyser.fftSize = 256;
57
+ this.analyser.smoothingTimeConstant = 0.3;
58
+ this.analyserData = new Uint8Array(this.analyser.frequencyBinCount);
59
+
60
+ // Chain: source → gainNode → analyser → destination
61
+ this.gainNode.connect(this.analyser);
62
+ this.analyser.connect(this.audioContext.destination);
63
+ }
64
+
65
+ /**
66
+ * Set TTS volume gain (1.0 = normal, 2.0 = 2x boost).
67
+ * Values above 1.0 amplify the signal for quieter devices.
68
+ */
69
+ setGain(value) {
70
+ this.gain = Math.max(0, Math.min(3.0, value));
71
+ if (this.gainNode) {
72
+ this.gainNode.gain.value = this.gain;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Ensure AudioContext is resumed (needed after iOS/browser auto-suspend).
78
+ */
79
+ async ensureRunning() {
80
+ if (!this.audioContext) await this.init();
81
+ if (this.audioContext.state === 'suspended') {
82
+ await this.audioContext.resume();
83
+ }
84
+ }
85
+
86
+ // -------------------------------------------------------------------------
87
+ // AudioContext path — play base64 audio directly via decodeAudioData
88
+ // -------------------------------------------------------------------------
89
+
90
+ /**
91
+ * Decode and play a base64-encoded WAV/MP3 via AudioContext.
92
+ * Drives the waveform analyser animation while playing.
93
+ * @param {string} audioBase64
94
+ * @returns {Promise<void>} Resolves when playback ends
95
+ */
96
+ async play(audioBase64) {
97
+ await this.ensureRunning();
98
+ this._notifySpeaking(true);
99
+
100
+ return new Promise((resolve) => {
101
+ try {
102
+ const arrayBuffer = this._base64ToArrayBuffer(audioBase64);
103
+
104
+ this.audioContext.decodeAudioData(arrayBuffer, (audioBuffer) => {
105
+ // Stop previous source if any
106
+ if (this.currentSource) {
107
+ try { this.currentSource.stop(); } catch (_) {}
108
+ }
109
+
110
+ const source = this.audioContext.createBufferSource();
111
+ source.buffer = audioBuffer;
112
+ source.connect(this.gainNode);
113
+
114
+ source.onended = () => {
115
+ this._stopAnalyserAnimation();
116
+ if (this.onAmplitude) this.onAmplitude(0);
117
+ this._notifySpeaking(false);
118
+ this.currentSource = null;
119
+ resolve();
120
+ };
121
+
122
+ this.currentSource = source;
123
+ source.start(0);
124
+ this._startAnalyserAnimation();
125
+
126
+ }, (err) => {
127
+ console.error('[TTSPlayer] decodeAudioData failed:', err);
128
+ this._notifySpeaking(false);
129
+ resolve();
130
+ });
131
+
132
+ } catch (error) {
133
+ console.error('[TTSPlayer] play() failed:', error);
134
+ this._notifySpeaking(false);
135
+ resolve();
136
+ }
137
+ });
138
+ }
139
+
140
+ // -------------------------------------------------------------------------
141
+ // Queue path — for streaming/chunked audio (HTMLAudioElement-based)
142
+ // -------------------------------------------------------------------------
143
+
144
+ /**
145
+ * Add a base64-encoded audio chunk to the queue and start playing if idle.
146
+ * @param {string} base64Audio
147
+ * @param {string} [mimeType='audio/wav']
148
+ */
149
+ queue(base64Audio, mimeType = 'audio/wav') {
150
+ try {
151
+ const blob = this._base64ToBlob(base64Audio, mimeType);
152
+ const url = URL.createObjectURL(blob);
153
+ const audio = new Audio(url);
154
+
155
+ audio.onended = () => {
156
+ URL.revokeObjectURL(url);
157
+ this._playNext();
158
+ };
159
+
160
+ audio.onerror = (e) => {
161
+ console.error('[TTSPlayer] Audio element error:', e);
162
+ URL.revokeObjectURL(url);
163
+ this._playNext();
164
+ };
165
+
166
+ this.audioQueue.push(audio);
167
+
168
+ if (!this.isPlaying) {
169
+ this._playNext();
170
+ }
171
+ } catch (error) {
172
+ console.error('[TTSPlayer] queue() failed:', error);
173
+ }
174
+ }
175
+
176
+ _playNext() {
177
+ if (this.audioQueue.length === 0) {
178
+ this.currentAudio = null;
179
+ this.isPlaying = false;
180
+ if (this.onAmplitude) this.onAmplitude(0);
181
+ this._notifySpeaking(false);
182
+ return;
183
+ }
184
+
185
+ this.currentAudio = this.audioQueue.shift();
186
+ this.isPlaying = true;
187
+ this._notifySpeaking(true);
188
+
189
+ // Route HTMLAudioElement through AudioContext gain for volume boost
190
+ if (this.gainNode && this.audioContext) {
191
+ try {
192
+ if (!this.currentAudio._mediaSource) {
193
+ this.currentAudio._mediaSource = this.audioContext.createMediaElementSource(this.currentAudio);
194
+ this.currentAudio._mediaSource.connect(this.gainNode);
195
+ }
196
+ } catch (e) {
197
+ // Fallback: if AudioContext routing fails, just play directly
198
+ console.warn('[TTSPlayer] MediaElementSource fallback:', e.message);
199
+ }
200
+ }
201
+
202
+ const promise = this.currentAudio.play();
203
+ if (promise) {
204
+ promise.catch(err => {
205
+ console.error('[TTSPlayer] Audio play blocked:', err.message);
206
+ this._playNext();
207
+ });
208
+ }
209
+ }
210
+
211
+ // -------------------------------------------------------------------------
212
+ // Stop / clear
213
+ // -------------------------------------------------------------------------
214
+
215
+ /**
216
+ * Stop all current and queued audio.
217
+ */
218
+ stop() {
219
+ // Stop AudioContext source
220
+ if (this.currentSource) {
221
+ try { this.currentSource.stop(); } catch (_) {}
222
+ this.currentSource = null;
223
+ }
224
+ this._stopAnalyserAnimation();
225
+
226
+ // Stop queue-based audio
227
+ if (this.currentAudio) {
228
+ this.currentAudio.pause();
229
+ this.currentAudio = null;
230
+ }
231
+ this.audioQueue = [];
232
+ this.isPlaying = false;
233
+
234
+ if (this.onAmplitude) this.onAmplitude(0);
235
+ this._notifySpeaking(false);
236
+ }
237
+
238
+ /**
239
+ * Release AudioContext resources.
240
+ */
241
+ destroy() {
242
+ this.stop();
243
+ if (this.audioContext) {
244
+ this.audioContext.close().catch(() => {});
245
+ this.audioContext = null;
246
+ this.analyser = null;
247
+ this.analyserData = null;
248
+ }
249
+ }
250
+
251
+ // -------------------------------------------------------------------------
252
+ // Waveform analyser animation
253
+ // -------------------------------------------------------------------------
254
+
255
+ _startAnalyserAnimation() {
256
+ if (this.analyserAnimationId) return;
257
+
258
+ const tick = () => {
259
+ if (!this.analyser) {
260
+ this.analyserAnimationId = null;
261
+ return;
262
+ }
263
+
264
+ this.analyser.getByteFrequencyData(this.analyserData);
265
+
266
+ // Average voice-range frequencies (lower 60% of bins)
267
+ const voiceRange = Math.floor(this.analyserData.length * 0.6);
268
+ let sum = 0;
269
+ for (let i = 0; i < voiceRange; i++) {
270
+ sum += this.analyserData[i];
271
+ }
272
+ const average = sum / voiceRange;
273
+ const normalized = average / 255;
274
+
275
+ // Boost so the mouth animation is visibly active
276
+ const boosted = normalized > 0.05
277
+ ? Math.max(0.3, normalized * 2.5)
278
+ : 0;
279
+
280
+ if (this.onAmplitude) this.onAmplitude(Math.min(1, boosted));
281
+
282
+ this.analyserAnimationId = requestAnimationFrame(tick);
283
+ };
284
+
285
+ tick();
286
+ }
287
+
288
+ _stopAnalyserAnimation() {
289
+ if (this.analyserAnimationId) {
290
+ cancelAnimationFrame(this.analyserAnimationId);
291
+ this.analyserAnimationId = null;
292
+ }
293
+ }
294
+
295
+ // -------------------------------------------------------------------------
296
+ // Helpers
297
+ // -------------------------------------------------------------------------
298
+
299
+ _notifySpeaking(isSpeaking) {
300
+ if (this.onSpeakingChange) this.onSpeakingChange(isSpeaking);
301
+ }
302
+
303
+ _base64ToArrayBuffer(base64) {
304
+ const binary = atob(base64);
305
+ const bytes = new Uint8Array(binary.length);
306
+ for (let i = 0; i < binary.length; i++) {
307
+ bytes[i] = binary.charCodeAt(i);
308
+ }
309
+ return bytes.buffer;
310
+ }
311
+
312
+ _base64ToBlob(base64, mimeType) {
313
+ const byteCharacters = atob(base64);
314
+ const byteNumbers = new Array(byteCharacters.length);
315
+ for (let i = 0; i < byteCharacters.length; i++) {
316
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
317
+ }
318
+ const byteArray = new Uint8Array(byteNumbers);
319
+ return new Blob([byteArray], { type: mimeType });
320
+ }
321
+ }
322
+
323
+ export default TTSPlayer;