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,288 @@
1
+ """
2
+ Workspace file browser — read-only access to the openclaw workspace directory.
3
+
4
+ GET /api/workspace/browse?path=<relative> → directory listing (files + dirs)
5
+ GET /api/workspace/file?path=<relative> → file content (text, 500 KB limit)
6
+ GET /api/workspace/raw?path=<relative> → raw file (images, PDFs, etc.)
7
+ GET /api/workspace/tree?path=<relative> → dirs only (for sidebar tree)
8
+ """
9
+
10
+ import os
11
+ from pathlib import Path
12
+ from flask import Blueprint, jsonify, request, send_file
13
+ from services.paths import RUNTIME_DIR
14
+
15
+ workspace_bp = Blueprint('workspace', __name__)
16
+
17
+ WORKSPACE_DIR = Path(os.getenv('WORKSPACE_DIR', str(RUNTIME_DIR / 'workspace')))
18
+
19
+ MAX_FILE_SIZE = 500 * 1024 # 500 KB preview limit
20
+
21
+ TEXT_EXTENSIONS = {
22
+ '.md', '.txt', '.json', '.csv', '.yaml', '.yml',
23
+ '.html', '.js', '.ts', '.py', '.sh', '.toml', '.log',
24
+ '.env', '.gitignore', '.sql', '.xml', '.ini', '.cfg',
25
+ '.lock', '.rst', '.njk', '.jsx', '.tsx',
26
+ }
27
+
28
+ IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico', '.bmp'}
29
+ MEDIA_EXTENSIONS = {'.mp3', '.wav', '.ogg', '.m4a', '.mp4', '.webm', '.mov', '.pdf'}
30
+ RAW_EXTENSIONS = IMAGE_EXTENSIONS | MEDIA_EXTENSIONS
31
+ MAX_RAW_SIZE = 20 * 1024 * 1024 # 20 MB limit for raw serving
32
+
33
+ HIDDEN_PREFIXES = {'.git', '__pycache__', 'node_modules', '.venv', 'venv'}
34
+
35
+ # Workspace-name → writable runtime path mapping
36
+ # workspace/Uploads/foo → RUNTIME_DIR/uploads/foo (writable)
37
+ # workspace/Agent/... → read-only, no mapping
38
+ _WRITABLE_MAP = {
39
+ 'Uploads': RUNTIME_DIR / 'uploads',
40
+ 'Canvas': RUNTIME_DIR / 'canvas-pages',
41
+ 'Music': RUNTIME_DIR / 'music',
42
+ 'AI-Music': RUNTIME_DIR / 'generated_music',
43
+ 'Transcripts': RUNTIME_DIR / 'transcripts',
44
+ 'Voice-Clones': RUNTIME_DIR / 'voice-clones',
45
+ 'Icons': RUNTIME_DIR / 'icons',
46
+ }
47
+
48
+
49
+ def _writable_path(rel: str):
50
+ """Map a workspace-relative path to its writable runtime path, or None if read-only."""
51
+ parts = rel.strip('/').split('/', 1)
52
+ if not parts or not parts[0]:
53
+ return None
54
+ prefix = parts[0]
55
+ base = _WRITABLE_MAP.get(prefix)
56
+ if base is None:
57
+ return None
58
+ sub = parts[1] if len(parts) > 1 else ''
59
+ target = (base / sub).resolve()
60
+ # Safety: ensure it stays within the writable base
61
+ try:
62
+ target.relative_to(base.resolve())
63
+ except ValueError:
64
+ return None
65
+ return target
66
+
67
+
68
+ def _resolve(rel: str):
69
+ """Resolve relative path safely within WORKSPACE_DIR. Returns None on traversal."""
70
+ try:
71
+ p = (WORKSPACE_DIR / rel.lstrip('/')).resolve()
72
+ p.relative_to(WORKSPACE_DIR.resolve()) # raises ValueError if outside
73
+ return p
74
+ except (ValueError, Exception):
75
+ return None
76
+
77
+
78
+ def _entry(item: Path, count_children: bool = True) -> dict:
79
+ """Build a single directory entry dict."""
80
+ try:
81
+ st = item.stat()
82
+ except (PermissionError, OSError):
83
+ return {
84
+ 'name': item.name,
85
+ 'type': 'dir' if item.is_dir() else 'file',
86
+ 'size': 0,
87
+ 'modified': 0,
88
+ 'ext': '',
89
+ }
90
+ e = {
91
+ 'name': item.name,
92
+ 'type': 'dir' if item.is_dir() else 'file',
93
+ 'size': st.st_size if item.is_file() else 0,
94
+ 'modified': int(st.st_mtime),
95
+ 'ext': item.suffix.lower() if item.is_file() else '',
96
+ }
97
+ if item.is_dir() and count_children:
98
+ try:
99
+ children = [c for c in item.iterdir() if not c.name.startswith('.')]
100
+ e['children'] = len(children)
101
+ except PermissionError:
102
+ e['children'] = 0
103
+ return e
104
+
105
+
106
+ def _is_hidden(name: str) -> bool:
107
+ return name.startswith('.') or name in HIDDEN_PREFIXES
108
+
109
+
110
+ @workspace_bp.route('/api/workspace/browse')
111
+ def browse():
112
+ rel = request.args.get('path', '')
113
+ target = _resolve(rel)
114
+
115
+ if target is None:
116
+ return jsonify({'error': 'Invalid path'}), 400
117
+
118
+ if not WORKSPACE_DIR.exists():
119
+ return jsonify({'path': '', 'entries': [], 'unavailable': True})
120
+
121
+ if not target.exists():
122
+ return jsonify({'error': 'Path not found'}), 404
123
+
124
+ if not target.is_dir():
125
+ return jsonify({'error': 'Not a directory'}), 400
126
+
127
+ try:
128
+ entries = []
129
+ for item in sorted(target.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower())):
130
+ if _is_hidden(item.name):
131
+ continue
132
+ try:
133
+ entries.append(_entry(item))
134
+ except (PermissionError, OSError):
135
+ continue
136
+ except PermissionError:
137
+ return jsonify({'error': 'Permission denied'}), 403
138
+
139
+ try:
140
+ rel_path = str(target.relative_to(WORKSPACE_DIR.resolve()))
141
+ if rel_path == '.':
142
+ rel_path = ''
143
+ except ValueError:
144
+ rel_path = ''
145
+
146
+ return jsonify({'path': rel_path, 'entries': entries})
147
+
148
+
149
+ @workspace_bp.route('/api/workspace/tree')
150
+ def tree():
151
+ """Return only directories for the sidebar tree (one level deep)."""
152
+ rel = request.args.get('path', '')
153
+ target = _resolve(rel)
154
+
155
+ if not WORKSPACE_DIR.exists():
156
+ return jsonify({'dirs': []})
157
+
158
+ if target is None or not target.exists() or not target.is_dir():
159
+ return jsonify({'dirs': []})
160
+
161
+ try:
162
+ dirs = []
163
+ for item in sorted(target.iterdir(), key=lambda p: p.name.lower()):
164
+ if item.is_dir() and not _is_hidden(item.name):
165
+ try:
166
+ rel_p = str(item.relative_to(WORKSPACE_DIR.resolve()))
167
+ except ValueError:
168
+ continue
169
+ # Check if this dir has subdirectories (for expand arrow)
170
+ try:
171
+ has_subdirs = any(
172
+ c.is_dir() and not _is_hidden(c.name)
173
+ for c in item.iterdir()
174
+ )
175
+ except PermissionError:
176
+ has_subdirs = False
177
+ dirs.append({'name': item.name, 'path': rel_p, 'has_subdirs': has_subdirs})
178
+ return jsonify({'dirs': dirs})
179
+ except Exception:
180
+ return jsonify({'dirs': []})
181
+
182
+
183
+ @workspace_bp.route('/api/workspace/file')
184
+ def read_file():
185
+ rel = request.args.get('path', '')
186
+ target = _resolve(rel)
187
+
188
+ if target is None:
189
+ return jsonify({'error': 'Invalid path'}), 400
190
+
191
+ if not target.exists():
192
+ return jsonify({'error': 'File not found'}), 404
193
+
194
+ if target.is_dir():
195
+ return jsonify({'error': 'Is a directory'}), 400
196
+
197
+ size = target.stat().st_size
198
+ ext = target.suffix.lower()
199
+
200
+ if size > MAX_FILE_SIZE:
201
+ return jsonify({
202
+ 'error': f'File too large for preview ({size // 1024} KB). Max is 500 KB.',
203
+ 'size': size,
204
+ 'too_large': True,
205
+ })
206
+
207
+ if ext not in TEXT_EXTENSIONS and ext != '':
208
+ return jsonify({'error': 'Binary file — no preview available', 'binary': True})
209
+
210
+ try:
211
+ content = target.read_text(encoding='utf-8', errors='replace')
212
+ return jsonify({
213
+ 'path': rel,
214
+ 'name': target.name,
215
+ 'ext': ext,
216
+ 'size': size,
217
+ 'content': content,
218
+ })
219
+ except Exception as e:
220
+ return jsonify({'error': str(e)}), 500
221
+
222
+
223
+ @workspace_bp.route('/api/workspace/raw')
224
+ def raw_file():
225
+ """Serve a raw file (images, audio, video, PDF) with correct content-type."""
226
+ rel = request.args.get('path', '')
227
+ target = _resolve(rel)
228
+
229
+ if target is None:
230
+ return jsonify({'error': 'Invalid path'}), 400
231
+
232
+ if not target.exists():
233
+ return jsonify({'error': 'File not found'}), 404
234
+
235
+ if target.is_dir():
236
+ return jsonify({'error': 'Is a directory'}), 400
237
+
238
+ size = target.stat().st_size
239
+ if size > MAX_RAW_SIZE:
240
+ return jsonify({'error': f'File too large ({size // (1024*1024)} MB). Max is 20 MB.'}), 413
241
+
242
+ try:
243
+ return send_file(str(target), conditional=True)
244
+ except Exception as e:
245
+ return jsonify({'error': str(e)}), 500
246
+
247
+
248
+ @workspace_bp.route('/api/workspace/mkdir', methods=['POST'])
249
+ def mkdir():
250
+ """Create a new folder inside a writable workspace area."""
251
+ import re
252
+ data = request.get_json(silent=True) or {}
253
+ rel = data.get('path', '').strip()
254
+ if not rel:
255
+ return jsonify({'error': 'Missing "path" field'}), 400
256
+
257
+ target = _writable_path(rel)
258
+ if target is None:
259
+ return jsonify({'error': 'Cannot create folders here (read-only area)'}), 403
260
+
261
+ # Validate the folder name (last component)
262
+ folder_name = target.name
263
+ if not folder_name or re.search(r'[<>:"|?*\\]', folder_name):
264
+ return jsonify({'error': 'Invalid folder name'}), 400
265
+ if folder_name.startswith('.'):
266
+ return jsonify({'error': 'Folder names cannot start with a dot'}), 400
267
+
268
+ if target.exists():
269
+ return jsonify({'error': 'Already exists'}), 409
270
+
271
+ try:
272
+ target.mkdir(parents=True, exist_ok=False)
273
+ return jsonify({'path': rel, 'created': True})
274
+ except PermissionError:
275
+ return jsonify({'error': 'Permission denied'}), 403
276
+ except Exception as e:
277
+ return jsonify({'error': str(e)}), 500
278
+
279
+
280
+ @workspace_bp.route('/api/workspace/writable')
281
+ def check_writable():
282
+ """Check if a workspace path is in a writable area."""
283
+ rel = request.args.get('path', '')
284
+ target = _writable_path(rel) if rel else None
285
+ # Root is not writable, but top-level writable dirs are
286
+ if not rel:
287
+ return jsonify({'writable': False, 'writable_areas': list(_WRITABLE_MAP.keys())})
288
+ return jsonify({'writable': target is not None, 'path': rel})