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,684 @@
1
+ /**
2
+ * Settings Panel Styles
3
+ * Matches app theme with dynamic colors
4
+ */
5
+
6
+ #settings-panel-container {
7
+ position: fixed;
8
+ top: 0;
9
+ left: 0;
10
+ width: 100%;
11
+ height: 100%;
12
+ z-index: 3000;
13
+ display: none;
14
+ align-items: center;
15
+ justify-content: center;
16
+ }
17
+
18
+ #settings-panel-container.open {
19
+ display: flex;
20
+ }
21
+
22
+ .settings-overlay {
23
+ position: absolute;
24
+ top: 0;
25
+ left: 0;
26
+ width: 100%;
27
+ height: 100%;
28
+ background: rgba(0, 0, 0, 0.8);
29
+ }
30
+
31
+ .settings-modal {
32
+ position: relative;
33
+ width: 90%;
34
+ max-width: 500px;
35
+ max-height: 80vh;
36
+ background: var(--panel-bg, #0a0a12);
37
+ border: 1px solid var(--blue-dim, #0055aa);
38
+ border-radius: 12px;
39
+ box-shadow: 0 0 30px rgba(0, 136, 255, 0.3);
40
+ overflow: hidden;
41
+ display: flex;
42
+ flex-direction: column;
43
+ }
44
+
45
+ .settings-header {
46
+ display: flex;
47
+ justify-content: space-between;
48
+ align-items: center;
49
+ padding: 16px 20px;
50
+ background: rgba(0, 136, 255, 0.1);
51
+ border-bottom: 1px solid var(--blue-dim, #0055aa);
52
+ }
53
+
54
+ .settings-header h2 {
55
+ color: var(--cyan, #00ffff);
56
+ font-size: 18px;
57
+ margin: 0;
58
+ font-family: 'Courier New', monospace;
59
+ }
60
+
61
+ .settings-close {
62
+ background: none;
63
+ border: none;
64
+ color: var(--blue, #0088ff);
65
+ font-size: 24px;
66
+ cursor: pointer;
67
+ padding: 0;
68
+ line-height: 1;
69
+ }
70
+
71
+ .settings-close:hover {
72
+ color: var(--cyan, #00ffff);
73
+ }
74
+
75
+ .settings-content {
76
+ padding: 20px;
77
+ overflow-y: auto;
78
+ flex: 1;
79
+ }
80
+
81
+ /* Settings Sections */
82
+ .settings-section {
83
+ margin-bottom: 20px;
84
+ }
85
+
86
+ .settings-section h3 {
87
+ color: var(--blue, #0088ff);
88
+ font-size: 14px;
89
+ margin: 0 0 12px 0;
90
+ cursor: pointer;
91
+ user-select: none;
92
+ display: flex;
93
+ align-items: center;
94
+ gap: 8px;
95
+ }
96
+
97
+ .settings-section h3:hover {
98
+ color: var(--cyan, #00ffff);
99
+ }
100
+
101
+ .section-arrow {
102
+ font-size: 10px;
103
+ transition: transform 0.2s;
104
+ }
105
+
106
+ .settings-section:not(.open) .section-content {
107
+ display: none;
108
+ }
109
+
110
+ .settings-section:not(.open) .section-arrow {
111
+ transform: rotate(-90deg);
112
+ }
113
+
114
+ .section-content {
115
+ padding-left: 18px;
116
+ }
117
+
118
+ /* Theme Presets */
119
+ .theme-presets label,
120
+ .theme-custom label,
121
+ .face-modes label {
122
+ display: block;
123
+ color: #888;
124
+ font-size: 11px;
125
+ text-transform: uppercase;
126
+ letter-spacing: 1px;
127
+ margin-bottom: 10px;
128
+ }
129
+
130
+ .preset-grid {
131
+ display: grid;
132
+ grid-template-columns: repeat(2, 1fr);
133
+ gap: 8px;
134
+ margin-bottom: 20px;
135
+ }
136
+
137
+ .preset-btn {
138
+ padding: 12px 16px;
139
+ border: 1px solid rgba(255, 255, 255, 0.2);
140
+ border-radius: 6px;
141
+ color: #fff;
142
+ font-size: 12px;
143
+ cursor: pointer;
144
+ transition: transform 0.15s, box-shadow 0.15s;
145
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
146
+ }
147
+
148
+ .preset-btn:hover {
149
+ transform: translateY(-2px);
150
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
151
+ }
152
+
153
+ /* Color Pickers */
154
+ .color-pickers {
155
+ display: flex;
156
+ gap: 20px;
157
+ margin-bottom: 16px;
158
+ }
159
+
160
+ .color-picker-group {
161
+ flex: 1;
162
+ }
163
+
164
+ .color-picker-group label {
165
+ margin-bottom: 6px;
166
+ }
167
+
168
+ .color-picker-group input[type="color"] {
169
+ width: 100%;
170
+ height: 40px;
171
+ border: 1px solid var(--blue-dim, #0055aa);
172
+ border-radius: 6px;
173
+ cursor: pointer;
174
+ background: transparent;
175
+ padding: 2px;
176
+ }
177
+
178
+ .color-picker-group input[type="color"]:hover {
179
+ border-color: var(--cyan, #00ffff);
180
+ }
181
+
182
+ .reset-btn {
183
+ width: 100%;
184
+ padding: 10px;
185
+ background: transparent;
186
+ border: 1px solid var(--blue-dim, #0055aa);
187
+ border-radius: 6px;
188
+ color: var(--blue, #0088ff);
189
+ cursor: pointer;
190
+ font-family: 'Courier New', monospace;
191
+ font-size: 12px;
192
+ transition: all 0.15s;
193
+ }
194
+
195
+ .reset-btn:hover {
196
+ border-color: var(--cyan, #00ffff);
197
+ color: var(--cyan, #00ffff);
198
+ }
199
+
200
+ /* Face Modes */
201
+ .mode-grid {
202
+ display: flex;
203
+ flex-direction: column;
204
+ gap: 8px;
205
+ }
206
+
207
+ .mode-btn {
208
+ display: flex;
209
+ flex-direction: column;
210
+ align-items: flex-start;
211
+ padding: 14px 16px;
212
+ background: rgba(0, 136, 255, 0.05);
213
+ border: 1px solid var(--blue-dim, #0055aa);
214
+ border-radius: 6px;
215
+ cursor: pointer;
216
+ transition: all 0.15s;
217
+ }
218
+
219
+ .mode-btn:hover {
220
+ background: rgba(0, 136, 255, 0.1);
221
+ border-color: var(--blue, #0088ff);
222
+ }
223
+
224
+ .mode-btn.active {
225
+ background: rgba(0, 136, 255, 0.15);
226
+ border-color: var(--cyan, #00ffff);
227
+ box-shadow: 0 0 10px rgba(0, 255, 255, 0.2);
228
+ }
229
+
230
+ .mode-name {
231
+ color: #fff;
232
+ font-size: 14px;
233
+ font-weight: bold;
234
+ margin-bottom: 4px;
235
+ }
236
+
237
+ .mode-desc {
238
+ color: #888;
239
+ font-size: 11px;
240
+ }
241
+
242
+ /* ── Face Picker Gallery ─────────────────────────────────────────────────── */
243
+
244
+ .face-picker-loading {
245
+ color: #555;
246
+ font-size: 12px;
247
+ padding: 12px 0;
248
+ font-family: 'Courier New', monospace;
249
+ }
250
+
251
+ .face-picker-gallery {
252
+ display: grid;
253
+ grid-template-columns: repeat(2, 1fr);
254
+ gap: 10px;
255
+ }
256
+
257
+ .face-card {
258
+ position: relative;
259
+ background: rgba(0, 136, 255, 0.04);
260
+ border: 1px solid var(--blue-dim, #0055aa);
261
+ border-radius: 8px;
262
+ overflow: hidden;
263
+ cursor: pointer;
264
+ transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s;
265
+ outline: none;
266
+ }
267
+
268
+ .face-card:hover {
269
+ border-color: var(--blue, #0088ff);
270
+ transform: translateY(-2px);
271
+ box-shadow: 0 4px 16px rgba(0, 136, 255, 0.2);
272
+ }
273
+
274
+ .face-card:focus-visible {
275
+ border-color: var(--cyan, #00ffff);
276
+ box-shadow: 0 0 0 2px rgba(0, 255, 255, 0.3);
277
+ }
278
+
279
+ .face-card--active {
280
+ border-color: var(--cyan, #00ffff);
281
+ box-shadow: 0 0 12px rgba(0, 255, 255, 0.25);
282
+ background: rgba(0, 255, 255, 0.06);
283
+ }
284
+
285
+ .face-card-media {
286
+ position: relative;
287
+ width: 100%;
288
+ aspect-ratio: 3 / 2;
289
+ background: #060610;
290
+ overflow: hidden;
291
+ }
292
+
293
+ .face-card-preview {
294
+ width: 100%;
295
+ height: 100%;
296
+ object-fit: cover;
297
+ display: block;
298
+ }
299
+
300
+ .face-card-preview--placeholder {
301
+ display: flex;
302
+ align-items: center;
303
+ justify-content: center;
304
+ width: 100%;
305
+ height: 100%;
306
+ background: linear-gradient(135deg, #0a0a18, #0d1a2e);
307
+ color: var(--blue, #0088ff);
308
+ font-size: 28px;
309
+ font-family: 'Courier New', monospace;
310
+ font-weight: bold;
311
+ }
312
+
313
+ .face-card-active-badge {
314
+ position: absolute;
315
+ top: 6px;
316
+ right: 6px;
317
+ background: var(--cyan, #00ffff);
318
+ color: #000;
319
+ font-size: 9px;
320
+ font-weight: bold;
321
+ letter-spacing: 0.5px;
322
+ text-transform: uppercase;
323
+ padding: 2px 6px;
324
+ border-radius: 3px;
325
+ font-family: 'Courier New', monospace;
326
+ }
327
+
328
+ .face-card-info {
329
+ padding: 10px 10px 8px;
330
+ }
331
+
332
+ .face-card-name {
333
+ color: #fff;
334
+ font-size: 13px;
335
+ font-weight: bold;
336
+ margin-bottom: 3px;
337
+ }
338
+
339
+ .face-card-desc {
340
+ color: #777;
341
+ font-size: 10px;
342
+ line-height: 1.4;
343
+ margin-bottom: 6px;
344
+ }
345
+
346
+ .face-card-moods {
347
+ display: flex;
348
+ gap: 4px;
349
+ flex-wrap: wrap;
350
+ margin-bottom: 5px;
351
+ }
352
+
353
+ .face-mood-dot {
354
+ width: 8px;
355
+ height: 8px;
356
+ border-radius: 50%;
357
+ background: var(--blue-dim, #0055aa);
358
+ display: inline-block;
359
+ cursor: default;
360
+ }
361
+
362
+ .face-card--active .face-mood-dot {
363
+ background: var(--cyan, #00ffff);
364
+ opacity: 0.6;
365
+ }
366
+
367
+ .face-card-features {
368
+ display: flex;
369
+ gap: 4px;
370
+ flex-wrap: wrap;
371
+ }
372
+
373
+ .face-feature-tag {
374
+ font-size: 9px;
375
+ color: #555;
376
+ border: 1px solid #222;
377
+ border-radius: 3px;
378
+ padding: 1px 5px;
379
+ font-family: 'Courier New', monospace;
380
+ }
381
+
382
+ .face-card--active .face-feature-tag {
383
+ color: #008866;
384
+ border-color: #005533;
385
+ }
386
+
387
+ /* =========================================================================
388
+ TTS Voice Preview (P4-T5)
389
+ ========================================================================= */
390
+
391
+ .tts-preview-hint {
392
+ font-size: 11px;
393
+ color: #888;
394
+ margin: 0 0 10px;
395
+ }
396
+
397
+ .tts-preview-loading,
398
+ .tts-preview-error,
399
+ .tts-preview-empty {
400
+ font-size: 12px;
401
+ color: #888;
402
+ padding: 10px 0;
403
+ }
404
+
405
+ .tts-preview-error {
406
+ color: #e05555;
407
+ }
408
+
409
+ .tts-voice-preview {
410
+ display: flex;
411
+ flex-direction: column;
412
+ gap: 16px;
413
+ }
414
+
415
+ .tts-provider-section {
416
+ border: 1px solid #1a1a1a;
417
+ border-radius: 6px;
418
+ padding: 10px 12px;
419
+ background: #0d0d0d;
420
+ }
421
+
422
+ .tts-provider-header {
423
+ display: flex;
424
+ align-items: center;
425
+ gap: 8px;
426
+ margin-bottom: 10px;
427
+ flex-wrap: wrap;
428
+ }
429
+
430
+ .tts-provider-name {
431
+ font-size: 12px;
432
+ font-weight: 600;
433
+ color: #ccc;
434
+ text-transform: uppercase;
435
+ letter-spacing: 0.5px;
436
+ }
437
+
438
+ .tts-provider-badge {
439
+ font-size: 9px;
440
+ padding: 1px 5px;
441
+ border-radius: 3px;
442
+ background: #003322;
443
+ color: #00bb77;
444
+ border: 1px solid #005533;
445
+ font-family: 'Courier New', monospace;
446
+ }
447
+
448
+ .tts-provider-meta {
449
+ font-size: 10px;
450
+ color: #555;
451
+ flex: 1;
452
+ min-width: 0;
453
+ overflow: hidden;
454
+ text-overflow: ellipsis;
455
+ white-space: nowrap;
456
+ }
457
+
458
+ .tts-voice-grid {
459
+ display: flex;
460
+ flex-wrap: wrap;
461
+ gap: 6px;
462
+ }
463
+
464
+ .tts-voice-card {
465
+ display: flex;
466
+ align-items: center;
467
+ gap: 5px;
468
+ padding: 6px 10px;
469
+ background: #141414;
470
+ border: 1px solid #2a2a2a;
471
+ border-radius: 5px;
472
+ cursor: pointer;
473
+ color: #bbb;
474
+ font-size: 12px;
475
+ transition: background 0.15s, border-color 0.15s, color 0.15s;
476
+ user-select: none;
477
+ }
478
+
479
+ .tts-voice-card:hover {
480
+ background: #1c1c1c;
481
+ border-color: #444;
482
+ color: #eee;
483
+ }
484
+
485
+ .tts-voice-play-icon {
486
+ font-size: 10px;
487
+ color: #555;
488
+ transition: color 0.15s;
489
+ }
490
+
491
+ .tts-voice-card:hover .tts-voice-play-icon {
492
+ color: #0088ff;
493
+ }
494
+
495
+ .tts-voice-card--loading {
496
+ border-color: #005599;
497
+ background: #0a1822;
498
+ color: #88bbee;
499
+ cursor: wait;
500
+ animation: tts-pulse 1s ease-in-out infinite;
501
+ }
502
+
503
+ .tts-voice-card--loading .tts-voice-play-icon::after {
504
+ content: '\2026'; /* ellipsis */
505
+ }
506
+
507
+ .tts-voice-card--playing {
508
+ border-color: #0088ff;
509
+ background: #001833;
510
+ color: #66aaff;
511
+ }
512
+
513
+ .tts-voice-card--playing .tts-voice-play-icon {
514
+ color: #0088ff;
515
+ content: '\25A0'; /* stop square */
516
+ }
517
+
518
+ .tts-voice-card--error {
519
+ border-color: #883333;
520
+ background: #1a0808;
521
+ color: #e07777;
522
+ }
523
+
524
+ @keyframes tts-pulse {
525
+ 0%, 100% { opacity: 1; }
526
+ 50% { opacity: 0.55; }
527
+ }
528
+
529
+ /* =========================================================================
530
+ Profile Switcher (P4-T6)
531
+ ========================================================================= */
532
+
533
+ .ps-loading,
534
+ .ps-empty {
535
+ font-size: 12px;
536
+ color: #556;
537
+ padding: 12px 0;
538
+ font-family: 'Courier New', monospace;
539
+ }
540
+
541
+ .ps-error {
542
+ font-size: 12px;
543
+ color: #c44;
544
+ padding: 12px 0;
545
+ }
546
+
547
+ .profile-switcher {
548
+ display: flex;
549
+ flex-direction: column;
550
+ gap: 10px;
551
+ }
552
+
553
+ .ps-grid {
554
+ display: flex;
555
+ flex-direction: column;
556
+ gap: 8px;
557
+ }
558
+
559
+ .ps-card {
560
+ display: flex;
561
+ align-items: flex-start;
562
+ gap: 12px;
563
+ width: 100%;
564
+ padding: 12px 14px;
565
+ background: rgba(0, 136, 255, 0.04);
566
+ border: 1px solid rgba(0, 136, 255, 0.15);
567
+ border-radius: 8px;
568
+ cursor: pointer;
569
+ text-align: left;
570
+ transition: background 0.15s, border-color 0.15s, box-shadow 0.15s;
571
+ position: relative;
572
+ }
573
+
574
+ .ps-card:hover {
575
+ background: rgba(0, 136, 255, 0.09);
576
+ border-color: rgba(0, 136, 255, 0.4);
577
+ }
578
+
579
+ .ps-card--active {
580
+ background: rgba(0, 255, 255, 0.05);
581
+ border-color: var(--cyan, #00ffff);
582
+ box-shadow: 0 0 10px rgba(0, 255, 255, 0.12);
583
+ }
584
+
585
+ .ps-card--active:hover {
586
+ background: rgba(0, 255, 255, 0.08);
587
+ }
588
+
589
+ .ps-card-icon {
590
+ font-size: 22px;
591
+ flex-shrink: 0;
592
+ line-height: 1;
593
+ margin-top: 2px;
594
+ }
595
+
596
+ .ps-card-body {
597
+ flex: 1;
598
+ min-width: 0;
599
+ }
600
+
601
+ .ps-card-name {
602
+ color: #dde;
603
+ font-size: 13px;
604
+ font-weight: 600;
605
+ margin-bottom: 4px;
606
+ display: flex;
607
+ align-items: center;
608
+ gap: 8px;
609
+ flex-wrap: wrap;
610
+ }
611
+
612
+ .ps-active-badge {
613
+ font-size: 9px;
614
+ font-weight: 700;
615
+ letter-spacing: 0.8px;
616
+ text-transform: uppercase;
617
+ color: #000;
618
+ background: var(--cyan, #00ffff);
619
+ padding: 1px 6px;
620
+ border-radius: 3px;
621
+ font-family: 'Courier New', monospace;
622
+ }
623
+
624
+ .ps-card-desc {
625
+ color: #667;
626
+ font-size: 11px;
627
+ line-height: 1.5;
628
+ margin-bottom: 6px;
629
+ }
630
+
631
+ .ps-card-meta {
632
+ display: flex;
633
+ align-items: center;
634
+ gap: 5px;
635
+ flex-wrap: wrap;
636
+ }
637
+
638
+ .ps-meta-item {
639
+ font-size: 10px;
640
+ color: #445;
641
+ font-family: 'Courier New', monospace;
642
+ }
643
+
644
+ .ps-meta-sep {
645
+ color: #334;
646
+ font-size: 10px;
647
+ }
648
+
649
+ .ps-card-check {
650
+ position: absolute;
651
+ top: 10px;
652
+ right: 12px;
653
+ color: var(--cyan, #00ffff);
654
+ font-size: 14px;
655
+ font-weight: bold;
656
+ line-height: 1;
657
+ }
658
+
659
+ .ps-status {
660
+ font-size: 12px;
661
+ min-height: 18px;
662
+ color: #556;
663
+ font-family: 'Courier New', monospace;
664
+ padding: 2px 0;
665
+ transition: color 0.2s;
666
+ }
667
+
668
+ .ps-status--pending { color: #7799cc; }
669
+ .ps-status--success { color: #44bb88; }
670
+ .ps-status--error { color: #cc4444; }
671
+
672
+ /* =========================================================================
673
+ Playlist Editor (P4-T2)
674
+ ========================================================================= */
675
+
676
+ .pe-loading,
677
+ .pe-error {
678
+ font-size: 12px;
679
+ color: #556;
680
+ padding: 12px 0;
681
+ font-family: 'Courier New', monospace;
682
+ }
683
+
684
+ .pe-error { color: #c44; }