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,213 @@
1
+ /**
2
+ * ThemeManager - Handles dynamic color theming
3
+ * Allows users to pick custom primary/accent colors
4
+ */
5
+
6
+ window.ThemeManager = {
7
+ // Current theme colors
8
+ colors: {
9
+ primary: '#0088ff', // Main blue
10
+ primaryDim: '#0055aa', // Darker blue
11
+ primaryBright: '#00aaff', // Brighter blue
12
+ accent: '#00ffff', // Cyan
13
+ accentDim: '#008888', // Darker cyan
14
+ },
15
+
16
+ // Preserved colors (don't change with theme)
17
+ fixedColors: {
18
+ green: '#00ff66',
19
+ yellow: '#ffdd00',
20
+ orange: '#ff6600',
21
+ red: '#ff2244',
22
+ purple: '#aa00ff',
23
+ },
24
+
25
+ // Default theme
26
+ defaultTheme: {
27
+ primary: '#0088ff',
28
+ accent: '#00ffff'
29
+ },
30
+
31
+ // Preset themes
32
+ presets: {
33
+ 'Classic Blue': { primary: '#0088ff', accent: '#00ffff' },
34
+ 'Neon Pink': { primary: '#ff0088', accent: '#ff66cc' },
35
+ 'Cyber Green': { primary: '#00ff88', accent: '#88ffcc' },
36
+ 'Purple Haze': { primary: '#8800ff', accent: '#cc66ff' },
37
+ 'Sunset Orange': { primary: '#ff6600', accent: '#ffaa00' },
38
+ 'Blood Red': { primary: '#cc0033', accent: '#ff6666' },
39
+ 'Matrix': { primary: '#00ff00', accent: '#88ff88' },
40
+ },
41
+
42
+ init() {
43
+ // Load saved theme from localStorage (immediate, no flash)
44
+ const saved = localStorage.getItem('ai-theme');
45
+ if (saved) {
46
+ try {
47
+ const theme = JSON.parse(saved);
48
+ this.colors.primary = theme.primary || this.defaultTheme.primary;
49
+ this.colors.accent = theme.accent || this.defaultTheme.accent;
50
+ } catch (e) {
51
+ console.warn('Failed to load saved theme:', e);
52
+ }
53
+ }
54
+
55
+ // Generate derived colors
56
+ this.updateDerivedColors();
57
+
58
+ // Apply theme immediately (avoid flash of default colors)
59
+ this.applyTheme();
60
+
61
+ // Sync with server in background (server is authoritative across devices)
62
+ this.loadFromServer();
63
+ },
64
+
65
+ loadFromServer() {
66
+ fetch('/api/theme')
67
+ .then(r => r.ok ? r.json() : null)
68
+ .then(theme => {
69
+ if (theme && theme.primary && theme.accent) {
70
+ this.colors.primary = theme.primary;
71
+ this.colors.accent = theme.accent;
72
+ this.updateDerivedColors();
73
+ this.applyTheme();
74
+ // Keep localStorage in sync
75
+ localStorage.setItem('ai-theme', JSON.stringify({
76
+ primary: theme.primary,
77
+ accent: theme.accent
78
+ }));
79
+ }
80
+ })
81
+ .catch(() => {
82
+ // Server unavailable — localStorage fallback already applied
83
+ });
84
+ },
85
+
86
+ updateDerivedColors() {
87
+ const p = this.colors.primary;
88
+ const a = this.colors.accent;
89
+
90
+ // Create dimmer versions (darker)
91
+ this.colors.primaryDim = this.darkenColor(p, 0.4);
92
+ this.colors.primaryBright = this.lightenColor(p, 0.3);
93
+ this.colors.accentDim = this.darkenColor(a, 0.4);
94
+ },
95
+
96
+ darkenColor(hex, factor) {
97
+ const rgb = this.hexToRgb(hex);
98
+ if (!rgb) return hex;
99
+ return this.rgbToHex(
100
+ Math.floor(rgb.r * (1 - factor)),
101
+ Math.floor(rgb.g * (1 - factor)),
102
+ Math.floor(rgb.b * (1 - factor))
103
+ );
104
+ },
105
+
106
+ lightenColor(hex, factor) {
107
+ const rgb = this.hexToRgb(hex);
108
+ if (!rgb) return hex;
109
+ return this.rgbToHex(
110
+ Math.min(255, Math.floor(rgb.r + (255 - rgb.r) * factor)),
111
+ Math.min(255, Math.floor(rgb.g + (255 - rgb.g) * factor)),
112
+ Math.min(255, Math.floor(rgb.b + (255 - rgb.b) * factor))
113
+ );
114
+ },
115
+
116
+ hexToRgb(hex) {
117
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
118
+ return result ? {
119
+ r: parseInt(result[1], 16),
120
+ g: parseInt(result[2], 16),
121
+ b: parseInt(result[3], 16)
122
+ } : null;
123
+ },
124
+
125
+ rgbToHex(r, g, b) {
126
+ return '#' + [r, g, b].map(x => {
127
+ const hex = x.toString(16);
128
+ return hex.length === 1 ? '0' + hex : hex;
129
+ }).join('');
130
+ },
131
+
132
+ setPrimaryColor(hex) {
133
+ this.colors.primary = hex;
134
+ this.updateDerivedColors();
135
+ this.applyTheme();
136
+ this.saveTheme();
137
+ },
138
+
139
+ setAccentColor(hex) {
140
+ this.colors.accent = hex;
141
+ this.updateDerivedColors();
142
+ this.applyTheme();
143
+ this.saveTheme();
144
+ },
145
+
146
+ applyPreset(presetName) {
147
+ const preset = this.presets[presetName];
148
+ if (preset) {
149
+ this.colors.primary = preset.primary;
150
+ this.colors.accent = preset.accent;
151
+ this.updateDerivedColors();
152
+ this.applyTheme();
153
+ this.saveTheme();
154
+ }
155
+ },
156
+
157
+ applyTheme() {
158
+ const root = document.documentElement;
159
+
160
+ // Update CSS variables
161
+ root.style.setProperty('--blue', this.colors.primary);
162
+ root.style.setProperty('--blue-dim', this.colors.primaryDim);
163
+ root.style.setProperty('--blue-bright', this.colors.primaryBright);
164
+ root.style.setProperty('--cyan', this.colors.accent);
165
+
166
+ // Also set as RGB values for rgba() usage
167
+ const primaryRgb = this.hexToRgb(this.colors.primary);
168
+ const accentRgb = this.hexToRgb(this.colors.accent);
169
+
170
+ if (primaryRgb) {
171
+ root.style.setProperty('--blue-rgb', `${primaryRgb.r}, ${primaryRgb.g}, ${primaryRgb.b}`);
172
+ }
173
+ if (accentRgb) {
174
+ root.style.setProperty('--cyan-rgb', `${accentRgb.r}, ${accentRgb.g}, ${accentRgb.b}`);
175
+ }
176
+
177
+ // Dispatch event for other modules to react
178
+ window.dispatchEvent(new CustomEvent('themeChanged', { detail: this.colors }));
179
+ },
180
+
181
+ saveTheme() {
182
+ const theme = { primary: this.colors.primary, accent: this.colors.accent };
183
+ localStorage.setItem('ai-theme', JSON.stringify(theme));
184
+ // Persist to server (best-effort)
185
+ fetch('/api/theme', {
186
+ method: 'POST',
187
+ headers: { 'Content-Type': 'application/json' },
188
+ body: JSON.stringify(theme),
189
+ }).catch(() => {});
190
+ },
191
+
192
+ resetTheme() {
193
+ this.colors.primary = this.defaultTheme.primary;
194
+ this.colors.accent = this.defaultTheme.accent;
195
+ this.updateDerivedColors();
196
+ this.applyTheme();
197
+ localStorage.setItem('ai-theme', JSON.stringify({
198
+ primary: this.colors.primary,
199
+ accent: this.colors.accent
200
+ }));
201
+ // Reset on server too
202
+ fetch('/api/theme/reset', { method: 'POST' }).catch(() => {});
203
+ },
204
+
205
+ getCurrentTheme() {
206
+ return {
207
+ primary: this.colors.primary,
208
+ accent: this.colors.accent,
209
+ primaryDim: this.colors.primaryDim,
210
+ primaryBright: this.colors.primaryBright,
211
+ };
212
+ }
213
+ };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * BaseVisualizer - Interface contract for swappable visualizer plugins
3
+ *
4
+ * Any visualizer that assigns to window.VisualizerModule must implement
5
+ * all methods listed here. MusicModule and the app init code call these.
6
+ *
7
+ * To create a new visualizer:
8
+ * 1. Copy this template
9
+ * 2. Implement all methods
10
+ * 3. Assign to window.VisualizerModule
11
+ * 4. Load via <script src="..."> before the main app script
12
+ */
13
+
14
+ window.BaseVisualizer = {
15
+ name: 'Base',
16
+ description: 'Template visualizer — does nothing. Override all methods.',
17
+
18
+ enabled: true,
19
+ autoplayEnabled: false,
20
+
21
+ async init() {},
22
+ async setupAnalyser() {},
23
+ startAnimation() {},
24
+ stopAnimation() {},
25
+ setEnabled(enabled) { this.enabled = enabled; },
26
+ setAutoplay(enabled) { this.autoplayEnabled = enabled; },
27
+ onTrackEnded() {},
28
+ updateToggleUI() {},
29
+ };
@@ -0,0 +1,291 @@
1
+ /* ===== PartyFXVisualizer CSS ===== */
2
+ /* Extracted from index.html - all styles for the Party FX music visualizer */
3
+
4
+ /* Autoplay toggle */
5
+ .autoplay-toggle {
6
+ display: flex;
7
+ align-items: center;
8
+ gap: 6px;
9
+ margin-left: 10px;
10
+ }
11
+ .autoplay-toggle label {
12
+ color: #888;
13
+ font-size: 0.7rem;
14
+ cursor: pointer;
15
+ }
16
+ .autoplay-toggle input[type="checkbox"] {
17
+ width: 14px;
18
+ height: 14px;
19
+ cursor: pointer;
20
+ accent-color: var(--cyan);
21
+ }
22
+ .autoplay-toggle.enabled label {
23
+ color: var(--cyan);
24
+ }
25
+
26
+ /* Visualizer toggle */
27
+ .visualizer-toggle {
28
+ display: flex;
29
+ align-items: center;
30
+ gap: 6px;
31
+ margin-left: 10px;
32
+ }
33
+ .visualizer-toggle label {
34
+ color: #888;
35
+ font-size: 0.7rem;
36
+ cursor: pointer;
37
+ }
38
+ .visualizer-toggle input[type="checkbox"] {
39
+ width: 14px;
40
+ height: 14px;
41
+ cursor: pointer;
42
+ accent-color: #aa00ff;
43
+ }
44
+ .visualizer-toggle.enabled label {
45
+ color: #aa00ff;
46
+ }
47
+
48
+ /* ===== PARTY EFFECTS ===== */
49
+ .party-effects-container {
50
+ position: fixed;
51
+ top: 0;
52
+ left: 0;
53
+ width: 100%;
54
+ height: 100%;
55
+ pointer-events: none;
56
+ z-index: 0;
57
+ overflow: hidden;
58
+ opacity: 0;
59
+ transition: opacity 0.3s ease;
60
+ }
61
+ .party-effects-container.active { opacity: 1; }
62
+
63
+ /* Center Glow - pulses behind face with audio */
64
+ .center-glow {
65
+ position: fixed;
66
+ top: 50%;
67
+ left: 50%;
68
+ transform: translate(-50%, -50%);
69
+ width: 600px;
70
+ height: 600px;
71
+ border-radius: 50%;
72
+ background: radial-gradient(circle, rgba(0, 136, 255, 0.15) 0%, rgba(0, 255, 255, 0.05) 40%, transparent 70%);
73
+ filter: blur(60px);
74
+ pointer-events: none;
75
+ z-index: 0;
76
+ }
77
+
78
+ /* Beat Flash - screen flash on bass hits */
79
+ .beat-flash {
80
+ position: fixed;
81
+ top: 0;
82
+ left: 0;
83
+ width: 100%;
84
+ height: 100%;
85
+ background: radial-gradient(circle, rgba(0, 255, 255, 0.3), transparent 70%);
86
+ pointer-events: none;
87
+ opacity: 0;
88
+ z-index: 1;
89
+ }
90
+
91
+ /* Screen shake on bass */
92
+ .shake {
93
+ animation: screenShake 0.1s ease-out;
94
+ }
95
+ @keyframes screenShake {
96
+ 0%, 100% { transform: translate(0, 0); }
97
+ 25% { transform: translate(calc(var(--shake-amount, 5px) * -1), var(--shake-amount, 5px)); }
98
+ 50% { transform: translate(var(--shake-amount, 5px), calc(var(--shake-amount, 5px) * -1)); }
99
+ 75% { transform: translate(calc(var(--shake-amount, 5px) * -1), calc(var(--shake-amount, 5px) * -1)); }
100
+ }
101
+
102
+ /* Ripple container */
103
+ .ripple-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
104
+ .sound-ripple {
105
+ position: absolute;
106
+ border: 2px solid var(--cyan);
107
+ border-radius: 50%;
108
+ opacity: 0;
109
+ animation: ripple-expand 1s ease-out forwards;
110
+ }
111
+ @keyframes ripple-expand {
112
+ 0% { width: 0; height: 0; opacity: 0.6; }
113
+ 100% { width: 300px; height: 300px; opacity: 0; }
114
+ }
115
+
116
+ /* Explosion container */
117
+ .explosion-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
118
+ .explosion-particle {
119
+ position: absolute;
120
+ width: 8px;
121
+ height: 8px;
122
+ background: var(--orange);
123
+ border-radius: 50%;
124
+ pointer-events: none;
125
+ }
126
+
127
+ /* Fireworks container */
128
+ .fireworks-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
129
+ .firework-particle {
130
+ position: absolute;
131
+ width: 6px;
132
+ height: 6px;
133
+ border-radius: 50%;
134
+ pointer-events: none;
135
+ }
136
+
137
+ /* Particles - orbit around center */
138
+ .party-particle-container {
139
+ position: absolute;
140
+ width: 100%;
141
+ height: 100%;
142
+ pointer-events: none;
143
+ }
144
+ .party-particle {
145
+ position: absolute;
146
+ width: 6px;
147
+ height: 6px;
148
+ background: var(--cyan);
149
+ border-radius: 50%;
150
+ opacity: 0;
151
+ will-change: transform, opacity;
152
+ }
153
+
154
+ /* Disco Dots - orbit and react */
155
+ .disco-container {
156
+ position: absolute;
157
+ width: 100%;
158
+ height: 100%;
159
+ pointer-events: none;
160
+ }
161
+ .disco-dot {
162
+ position: absolute;
163
+ width: 15px;
164
+ height: 15px;
165
+ background: white;
166
+ border-radius: 50%;
167
+ filter: blur(3px);
168
+ opacity: 0;
169
+ will-change: transform, opacity;
170
+ }
171
+
172
+ /* Waveform oscilloscope - FULL SCREEN background effect */
173
+ .oscilloscope-container {
174
+ position: fixed;
175
+ top: 0;
176
+ left: 0;
177
+ width: 100vw;
178
+ height: 100vh;
179
+ opacity: 0;
180
+ pointer-events: none;
181
+ z-index: 0;
182
+ transition: opacity 0.3s;
183
+ }
184
+ .oscilloscope-container.active { opacity: 1; }
185
+ #oscilloscope-canvas {
186
+ width: 100%;
187
+ height: 100%;
188
+ filter: drop-shadow(0 0 30px var(--cyan)) drop-shadow(0 0 60px var(--blue));
189
+ }
190
+
191
+ /* ===== VISUALIZER BARS - Equalizer bars around face box ===== */
192
+
193
+ /* Top/Bottom visualizers - attached to face box */
194
+ .visualizer-container {
195
+ position: absolute;
196
+ left: 10px;
197
+ right: 10px;
198
+ height: 60px;
199
+ display: flex;
200
+ justify-content: space-between;
201
+ align-items: flex-end;
202
+ gap: 3px;
203
+ pointer-events: none;
204
+ z-index: -1;
205
+ opacity: 0;
206
+ transition: opacity 0.3s ease;
207
+ }
208
+ .visualizer-container.active { opacity: 1; }
209
+ .visualizer-container.top {
210
+ top: -8px;
211
+ transform: translateY(-100%);
212
+ align-items: flex-end;
213
+ }
214
+ .visualizer-container.bottom {
215
+ bottom: -8px;
216
+ transform: translateY(100%);
217
+ align-items: flex-start;
218
+ }
219
+
220
+ .visualizer-bar {
221
+ flex: 1;
222
+ max-width: 8px;
223
+ border-radius: 3px;
224
+ transition: height 0.05s ease;
225
+ box-shadow: 0 0 8px var(--cyan);
226
+ }
227
+ .visualizer-container.top .visualizer-bar {
228
+ background: linear-gradient(to top,
229
+ var(--cyan) 0%,
230
+ var(--blue) 40%,
231
+ #aa00ff 70%,
232
+ var(--red) 100%);
233
+ background-size: 100% 55px;
234
+ background-position: bottom;
235
+ background-repeat: no-repeat;
236
+ }
237
+ .visualizer-container.bottom .visualizer-bar {
238
+ background: linear-gradient(to bottom,
239
+ var(--cyan) 0%,
240
+ var(--blue) 40%,
241
+ #aa00ff 70%,
242
+ var(--red) 100%);
243
+ background-size: 100% 55px;
244
+ background-position: top;
245
+ background-repeat: no-repeat;
246
+ }
247
+
248
+ /* Side visualizers - attached to face box, full height */
249
+ .side-visualizer {
250
+ position: absolute;
251
+ top: 10px;
252
+ bottom: 10px;
253
+ display: flex;
254
+ flex-direction: column;
255
+ justify-content: space-between;
256
+ z-index: -1;
257
+ opacity: 0;
258
+ transition: opacity 0.3s ease;
259
+ pointer-events: none;
260
+ }
261
+ .side-visualizer.active { opacity: 1; }
262
+ .side-visualizer.left { right: 100%; margin-right: 8px; align-items: flex-end; }
263
+ .side-visualizer.right { left: 100%; margin-left: 8px; align-items: flex-start; }
264
+
265
+ .side-bar {
266
+ width: 80px;
267
+ height: 6px;
268
+ border-radius: 3px;
269
+ box-shadow: 0 0 8px var(--cyan);
270
+ transition: width 0.05s ease;
271
+ }
272
+ .side-visualizer.left .side-bar {
273
+ background: linear-gradient(to left,
274
+ var(--cyan) 0%,
275
+ var(--blue) 40%,
276
+ #aa00ff 70%,
277
+ var(--red) 100%);
278
+ background-size: 85px 100%;
279
+ background-position: right;
280
+ background-repeat: no-repeat;
281
+ }
282
+ .side-visualizer.right .side-bar {
283
+ background: linear-gradient(to right,
284
+ var(--cyan) 0%,
285
+ var(--blue) 40%,
286
+ #aa00ff 70%,
287
+ var(--red) 100%);
288
+ background-size: 85px 100%;
289
+ background-position: left;
290
+ background-repeat: no-repeat;
291
+ }