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,174 @@
1
+ /**
2
+ * Supertonic TTS Provider
3
+ * Handles text-to-speech via the Supertonic backend
4
+ */
5
+ import { BaseTTSProvider } from './BaseTTSProvider.js';
6
+
7
+ export class SupertonicProvider extends BaseTTSProvider {
8
+ constructor(config = {}) {
9
+ super(config);
10
+ this.name = 'supertonic';
11
+ this.serverUrl = config.serverUrl || '';
12
+ this.voices = ['M1', 'M2', 'F1', 'F2']; // Available Supertonic voices
13
+ this.currentVoice = config.voice || 'M1';
14
+ this.audioQueue = [];
15
+ this.currentAudio = null;
16
+ this.audioContext = null;
17
+ }
18
+
19
+ async init() {
20
+ console.log('[Supertonic] Initializing...');
21
+ // Pre-warm audio context on user gesture
22
+ this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
23
+ console.log('[Supertonic] Ready with voices:', this.voices);
24
+ return true;
25
+ }
26
+
27
+ getVoices() {
28
+ return this.voices;
29
+ }
30
+
31
+ setVoice(voiceName) {
32
+ if (this.voices.includes(voiceName)) {
33
+ this.currentVoice = voiceName;
34
+ console.log('[Supertonic] Voice set to:', voiceName);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Synthesize speech by calling the backend API
40
+ * @param {string} text - Text to speak
41
+ * @param {object} options - Optional parameters (speed, lang)
42
+ * @returns {Promise<boolean>} Success
43
+ */
44
+ async speak(text, options = {}) {
45
+ if (!text || !text.trim()) {
46
+ console.warn('[Supertonic] Empty text, skipping');
47
+ return false;
48
+ }
49
+
50
+ const payload = {
51
+ text: text.trim(),
52
+ provider: 'supertonic',
53
+ voice: this.currentVoice,
54
+ speed: options.speed || 1.05,
55
+ lang: options.lang || 'en'
56
+ };
57
+
58
+ try {
59
+ console.log('[Supertonic] Requesting TTS for:', text.substring(0, 50) + '...');
60
+
61
+ const response = await fetch(`${this.serverUrl}/api/tts/generate`, {
62
+ method: 'POST',
63
+ headers: { 'Content-Type': 'application/json' },
64
+ body: JSON.stringify(payload)
65
+ });
66
+
67
+ if (!response.ok) {
68
+ const errorData = await response.json().catch(() => ({}));
69
+ throw new Error(errorData.error || `TTS request failed: ${response.status}`);
70
+ }
71
+
72
+ // Response is WAV audio directly, not JSON
73
+ const audioBlob = await response.blob();
74
+ const audioUrl = URL.createObjectURL(audioBlob);
75
+
76
+ this.queueAudioUrl(audioUrl);
77
+ return true;
78
+
79
+ } catch (error) {
80
+ console.error('[Supertonic] TTS error:', error);
81
+ return false;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Queue audio URL for playback
87
+ * @param {string} audioUrl - Blob URL for audio
88
+ */
89
+ queueAudioUrl(audioUrl) {
90
+ this.audioQueue.push(audioUrl);
91
+
92
+ if (!this.isPlaying) {
93
+ this.playNextAudio();
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Play next audio in queue
99
+ */
100
+ async playNextAudio() {
101
+ if (this.audioQueue.length === 0) {
102
+ this.isPlaying = false;
103
+ this.currentAudio = null;
104
+ return;
105
+ }
106
+
107
+ this.isPlaying = true;
108
+ const audioUrl = this.audioQueue.shift();
109
+
110
+ try {
111
+ const audio = new Audio(audioUrl);
112
+
113
+ audio.onended = () => {
114
+ URL.revokeObjectURL(audioUrl);
115
+ this.playNextAudio();
116
+ };
117
+
118
+ audio.onerror = (e) => {
119
+ console.error('[Supertonic] Audio playback error:', e);
120
+ URL.revokeObjectURL(audioUrl);
121
+ this.playNextAudio();
122
+ };
123
+
124
+ this.currentAudio = audio;
125
+ await audio.play();
126
+ } catch (error) {
127
+ console.error('[Supertonic] Failed to play audio:', error);
128
+ URL.revokeObjectURL(audioUrl);
129
+ this.playNextAudio();
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Convert base64 to Blob
135
+ */
136
+ base64ToBlob(base64, mimeType) {
137
+ const byteCharacters = atob(base64);
138
+ const byteNumbers = new Array(byteCharacters.length);
139
+ for (let i = 0; i < byteCharacters.length; i++) {
140
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
141
+ }
142
+ const byteArray = new Uint8Array(byteNumbers);
143
+ return new Blob([byteArray], { type: mimeType });
144
+ }
145
+
146
+ /**
147
+ * Stop all audio playback
148
+ */
149
+ stop() {
150
+ this.audioQueue = [];
151
+ if (this.currentAudio) {
152
+ this.currentAudio.pause();
153
+ this.currentAudio = null;
154
+ }
155
+ this.isPlaying = false;
156
+ }
157
+
158
+ isReady() {
159
+ return true;
160
+ }
161
+
162
+ getCostPerMinute() {
163
+ return 0; // Free
164
+ }
165
+
166
+ destroy() {
167
+ this.stop();
168
+ if (this.audioContext) {
169
+ this.audioContext.close();
170
+ }
171
+ }
172
+ }
173
+
174
+ export default SupertonicProvider;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * TTS Provider Registry
3
+ * Manages available TTS providers and handles switching
4
+ */
5
+ import { SupertonicProvider } from './SupertonicProvider.js';
6
+ import { HumeProvider } from './HumeProvider.js';
7
+
8
+ class TTSProviderRegistry {
9
+ constructor() {
10
+ this.providers = new Map();
11
+ this.currentProvider = null;
12
+ this.currentProviderId = null;
13
+ }
14
+
15
+ /**
16
+ * Register a TTS provider
17
+ * @param {string} id - Unique provider ID
18
+ * @param {BaseTTSProvider} provider - Provider instance
19
+ */
20
+ register(id, provider) {
21
+ this.providers.set(id, provider);
22
+ console.log(`[TTSRegistry] Registered provider: ${id}`);
23
+ }
24
+
25
+ /**
26
+ * Get a provider by ID
27
+ * @param {string} id
28
+ * @returns {BaseTTSProvider|null}
29
+ */
30
+ get(id) {
31
+ return this.providers.get(id) || null;
32
+ }
33
+
34
+ /**
35
+ * Get current provider
36
+ * @returns {BaseTTSProvider|null}
37
+ */
38
+ getCurrent() {
39
+ return this.currentProvider;
40
+ }
41
+
42
+ /**
43
+ * Switch to a different provider
44
+ * @param {string} id
45
+ * @returns {boolean} Success
46
+ */
47
+ switchTo(id) {
48
+ const provider = this.providers.get(id);
49
+ if (!provider) {
50
+ console.error(`[TTSRegistry] Provider not found: ${id}`);
51
+ return false;
52
+ }
53
+
54
+ // Stop current provider if different
55
+ if (this.currentProvider && this.currentProviderId !== id) {
56
+ this.currentProvider.stop();
57
+ }
58
+
59
+ this.currentProvider = provider;
60
+ this.currentProviderId = id;
61
+ console.log(`[TTSRegistry] Switched to: ${id}`);
62
+ return true;
63
+ }
64
+
65
+ /**
66
+ * Get all registered provider IDs
67
+ * @returns {string[]}
68
+ */
69
+ getProviderIds() {
70
+ return Array.from(this.providers.keys());
71
+ }
72
+
73
+ /**
74
+ * Speak text using current provider
75
+ * @param {string} text
76
+ * @param {object} options
77
+ * @returns {Promise<boolean>}
78
+ */
79
+ async speak(text, options = {}) {
80
+ if (!this.currentProvider) {
81
+ console.error('[TTSRegistry] No provider selected');
82
+ return false;
83
+ }
84
+ return this.currentProvider.speak(text, options);
85
+ }
86
+
87
+ /**
88
+ * Stop current playback
89
+ */
90
+ stop() {
91
+ if (this.currentProvider) {
92
+ this.currentProvider.stop();
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Set voice on current provider
98
+ * @param {string} voice
99
+ */
100
+ setVoice(voice) {
101
+ if (this.currentProvider) {
102
+ this.currentProvider.setVoice(voice);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Get voices for current provider
108
+ * @returns {string[]}
109
+ */
110
+ getVoices() {
111
+ if (this.currentProvider) {
112
+ return this.currentProvider.getVoices();
113
+ }
114
+ return [];
115
+ }
116
+
117
+ /**
118
+ * Initialize all providers
119
+ */
120
+ async initAll(config) {
121
+ // Create and register providers
122
+ const supertonic = new SupertonicProvider(config);
123
+ const hume = new HumeProvider(config);
124
+
125
+ await supertonic.init();
126
+ await hume.init();
127
+
128
+ this.register('supertonic', supertonic);
129
+ this.register('hume', hume);
130
+
131
+ // Default to supertonic
132
+ this.switchTo('supertonic');
133
+
134
+ console.log('[TTSRegistry] All providers initialized');
135
+ }
136
+ }
137
+
138
+ // Export singleton
139
+ export const ttsRegistry = new TTSProviderRegistry();
140
+ export default ttsRegistry;
@@ -0,0 +1,154 @@
1
+ /**
2
+ * adapter-registry.js — Adapter ID → module path mapping + dynamic import (P6-T5)
3
+ *
4
+ * The AdapterRegistry knows which JS file corresponds to each adapter ID
5
+ * declared in a profile's "adapter" field. It handles dynamic import with
6
+ * caching so each module is only fetched once, even when multiple profiles
7
+ * reference the same adapter.
8
+ *
9
+ * Adding a new adapter system:
10
+ * 1. Create src/adapters/my-adapter.js (copy _template.js)
11
+ * 2. Add an entry to ADAPTER_PATHS below
12
+ * 3. Create profiles/my-profile.json with "adapter": "my-adapter"
13
+ * Done — no other changes needed.
14
+ *
15
+ * Ref: future-dev-plans/17-MULTI-AGENT-FRAMEWORK.md (Plug-and-Play section)
16
+ *
17
+ * Usage:
18
+ * import { adapterRegistry } from './adapter-registry.js';
19
+ * const adapter = await adapterRegistry.load('hume-evi');
20
+ */
21
+
22
+ // ─────────────────────────────────────────────────────────────────────────────
23
+ // Static adapter map — adapter ID → relative path from this file (src/shell/)
24
+ // ─────────────────────────────────────────────────────────────────────────────
25
+
26
+ const ADAPTER_PATHS = {
27
+ 'clawdbot': '../adapters/ClawdBotAdapter.js',
28
+ 'hume-evi': '../adapters/hume-evi.js',
29
+ // Add future adapters here:
30
+ // 'elevenlabs-classic': '../adapters/elevenlabs-classic.js',
31
+ // 'hybrid': '../adapters/hybrid.js',
32
+ // 'openai-realtime': '../adapters/openai-realtime.js',
33
+ };
34
+
35
+ /** Default adapter used when a profile omits the "adapter" field. */
36
+ const DEFAULT_ADAPTER_ID = 'clawdbot';
37
+
38
+ // ─────────────────────────────────────────────────────────────────────────────
39
+ // AdapterRegistry
40
+ // ─────────────────────────────────────────────────────────────────────────────
41
+
42
+ class AdapterRegistry {
43
+ constructor() {
44
+ /**
45
+ * Cache of already-loaded adapter objects, keyed by adapter ID.
46
+ * @type {Object.<string, object>}
47
+ */
48
+ this._cache = {};
49
+ }
50
+
51
+ /**
52
+ * Load an adapter by its ID.
53
+ *
54
+ * Resolves to the adapter object (the object with init/start/stop/destroy
55
+ * exported as `default` from the adapter module, or as a named export).
56
+ *
57
+ * Results are cached so the module is only dynamically imported once.
58
+ *
59
+ * @param {string} [adapterId] - Adapter ID as declared in a profile's "adapter" field.
60
+ * Falls back to DEFAULT_ADAPTER_ID if omitted or unknown.
61
+ * @returns {Promise<object>} Adapter object
62
+ */
63
+ async load(adapterId) {
64
+ const id = adapterId || DEFAULT_ADAPTER_ID;
65
+
66
+ // Return cached module if already loaded
67
+ if (this._cache[id]) return this._cache[id];
68
+
69
+ const path = ADAPTER_PATHS[id];
70
+
71
+ if (!path) {
72
+ console.warn(
73
+ `[AdapterRegistry] Unknown adapter "${id}" — falling back to "${DEFAULT_ADAPTER_ID}".`,
74
+ `Known adapters: [${this.knownAdapters().join(', ')}]`
75
+ );
76
+ // Avoid infinite recursion if default itself is unknown
77
+ if (id === DEFAULT_ADAPTER_ID) {
78
+ throw new Error(`[AdapterRegistry] Default adapter "${DEFAULT_ADAPTER_ID}" has no registered path.`);
79
+ }
80
+ return this.load(DEFAULT_ADAPTER_ID);
81
+ }
82
+
83
+ try {
84
+ const module = await import(path);
85
+
86
+ // Prefer default export; fall back to first named export that looks like an adapter
87
+ const adapter =
88
+ module.default ||
89
+ Object.values(module).find(
90
+ v => v && typeof v === 'object' && typeof v.init === 'function'
91
+ );
92
+
93
+ if (!adapter) {
94
+ throw new Error(`No adapter export found in "${path}"`);
95
+ }
96
+
97
+ this._cache[id] = adapter;
98
+ console.log(`[AdapterRegistry] Loaded adapter: "${id}" from ${path}`);
99
+ return adapter;
100
+
101
+ } catch (err) {
102
+ console.error(`[AdapterRegistry] Failed to load adapter "${id}":`, err);
103
+
104
+ // If loading the requested adapter fails, try the default as a fallback
105
+ if (id !== DEFAULT_ADAPTER_ID) {
106
+ console.warn(`[AdapterRegistry] Falling back to default adapter "${DEFAULT_ADAPTER_ID}"`);
107
+ return this.load(DEFAULT_ADAPTER_ID);
108
+ }
109
+
110
+ throw err; // Re-throw if even the default adapter fails
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Return true if adapterId is registered in the static map.
116
+ * @param {string} adapterId
117
+ * @returns {boolean}
118
+ */
119
+ has(adapterId) {
120
+ return adapterId in ADAPTER_PATHS;
121
+ }
122
+
123
+ /**
124
+ * Return the list of all registered adapter IDs.
125
+ * @returns {string[]}
126
+ */
127
+ knownAdapters() {
128
+ return Object.keys(ADAPTER_PATHS);
129
+ }
130
+
131
+ /**
132
+ * The default adapter ID used when a profile omits "adapter".
133
+ * @type {string}
134
+ */
135
+ get defaultAdapter() {
136
+ return DEFAULT_ADAPTER_ID;
137
+ }
138
+
139
+ /**
140
+ * Clear the module cache (primarily for testing).
141
+ */
142
+ clearCache() {
143
+ this._cache = {};
144
+ }
145
+ }
146
+
147
+ // ─────────────────────────────────────────────────────────────────────────────
148
+ // Singleton — one registry for the whole app
149
+ // ─────────────────────────────────────────────────────────────────────────────
150
+
151
+ export const adapterRegistry = new AdapterRegistry();
152
+
153
+ // Also export class and constants for testing / multiple instances
154
+ export { AdapterRegistry, ADAPTER_PATHS, DEFAULT_ADAPTER_ID };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * caller-bridge.js — Connect caller phone effect to EventBridge (P6-T4)
3
+ *
4
+ * Only connected when the active adapter has 'caller_effects' capability.
5
+ * Also blocks music sync during active caller skits.
6
+ *
7
+ * Ref: future-dev-plans/17-MULTI-AGENT-FRAMEWORK.md (App Shell section)
8
+ */
9
+
10
+ import { AgentEvents } from '../core/EventBridge.js';
11
+
12
+ /**
13
+ * Wire caller phone effect to the EventBridge.
14
+ * Only call this if the active adapter has 'caller_effects' capability.
15
+ *
16
+ * @param {import('../core/EventBridge.js').EventBridge} bridge
17
+ * @param {string[]} capabilities - active adapter capabilities
18
+ * @returns {Function[]} unsubscribe functions
19
+ */
20
+ export function connectCallerEffect(bridge, capabilities) {
21
+ if (!capabilities.includes('caller_effects')) return [];
22
+
23
+ const unsubs = [
24
+ bridge.on(AgentEvents.CALLER_EFFECT, ({ enabled }) => {
25
+ window.setCallerEffect?.(enabled);
26
+
27
+ // Block music sync during caller skit
28
+ if (window.musicPlayer) {
29
+ window.musicPlayer.syncBlocked = enabled;
30
+ }
31
+ }),
32
+ ];
33
+
34
+ return unsubs;
35
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * camera-bridge.js — Connect CameraModule to EventBridge (P6-T4)
3
+ *
4
+ * Only connected when the active adapter has 'camera' capability.
5
+ * Currently a placeholder — camera captures are driven by the
6
+ * CameraModule internally. This bridge handles future vision events.
7
+ *
8
+ * Ref: future-dev-plans/17-MULTI-AGENT-FRAMEWORK.md (App Shell section)
9
+ */
10
+
11
+ import { AgentEvents } from '../core/EventBridge.js';
12
+
13
+ /**
14
+ * Wire CameraModule to the EventBridge.
15
+ * Only call this if the active adapter has 'camera' capability.
16
+ *
17
+ * @param {import('../core/EventBridge.js').EventBridge} bridge
18
+ * @param {string[]} capabilities - active adapter capabilities
19
+ * @returns {Function[]} unsubscribe functions
20
+ */
21
+ export function connectCamera(bridge, capabilities) {
22
+ if (!capabilities.includes('camera')) return [];
23
+
24
+ // Camera button visibility is handled by _updateFeatureUI in orchestrator.
25
+ // Future: subscribe to VISION_RESULT events here if adapters emit them.
26
+
27
+ return [];
28
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * canvas-bridge.js — Connect CanvasControl to EventBridge (P6-T4)
3
+ *
4
+ * Only connected when the active adapter has 'canvas' capability.
5
+ * Routes CANVAS_CMD events to the canvas display system.
6
+ *
7
+ * Ref: future-dev-plans/17-MULTI-AGENT-FRAMEWORK.md (App Shell section)
8
+ */
9
+
10
+ import { AgentEvents } from '../core/EventBridge.js';
11
+
12
+ /**
13
+ * Wire CanvasControl to the EventBridge.
14
+ * Only call this if the active adapter has 'canvas' capability.
15
+ *
16
+ * @param {import('../core/EventBridge.js').EventBridge} bridge
17
+ * @param {string[]} capabilities - active adapter capabilities
18
+ * @returns {Function[]} unsubscribe functions
19
+ */
20
+ export function connectCanvas(bridge, capabilities) {
21
+ if (!capabilities.includes('canvas')) return [];
22
+
23
+ const unsubs = [
24
+ bridge.on(AgentEvents.CANVAS_CMD, ({ action, url }) => {
25
+ if (!window.CanvasControl) return;
26
+ if (action === 'present') window.CanvasControl.updateDisplay('html', url);
27
+ else if (action === 'close') window.CanvasControl.hide();
28
+ }),
29
+ ];
30
+
31
+ return unsubs;
32
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * commercial-bridge.js — Connect commercial system to EventBridge (P6-T4)
3
+ *
4
+ * Only connected when the active adapter has 'commercials' capability.
5
+ * Handles commercial playback: stops music, plays commercial,
6
+ * then prompts agent to resume.
7
+ *
8
+ * Ref: future-dev-plans/17-MULTI-AGENT-FRAMEWORK.md (App Shell section)
9
+ */
10
+
11
+ import { AgentEvents, AgentActions } from '../core/EventBridge.js';
12
+
13
+ /**
14
+ * Wire commercial system to the EventBridge.
15
+ * Only call this if the active adapter has 'commercials' capability.
16
+ *
17
+ * @param {import('../core/EventBridge.js').EventBridge} bridge
18
+ * @param {string[]} capabilities - active adapter capabilities
19
+ * @returns {Function[]} unsubscribe functions
20
+ */
21
+ export function connectCommercial(bridge, capabilities) {
22
+ if (!capabilities.includes('commercials')) return [];
23
+
24
+ const unsubs = [
25
+ bridge.on(AgentEvents.COMMERCIAL, async ({ action }) => {
26
+ if (action !== 'play') return;
27
+
28
+ // Stop music for commercial break
29
+ window.musicPlayer?.stop?.();
30
+
31
+ // Play commercial (global function from legacy code)
32
+ if (window.playCommercial) {
33
+ await window.playCommercial();
34
+ }
35
+
36
+ // Tell agent commercial is over
37
+ bridge.emit(AgentActions.FORCE_MESSAGE, {
38
+ text: "[SYSTEM: Commercial over! Say we're back and play the next track!]",
39
+ });
40
+ }),
41
+ ];
42
+
43
+ return unsubs;
44
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * face-bridge.js — Connect FaceModule to EventBridge (P6-T4)
3
+ *
4
+ * Subscribes to agent events and drives face state changes.
5
+ * Returns an array of unsubscribe functions for clean teardown.
6
+ *
7
+ * Ref: future-dev-plans/17-MULTI-AGENT-FRAMEWORK.md (App Shell section)
8
+ */
9
+
10
+ import { AgentEvents } from '../core/EventBridge.js';
11
+
12
+ /**
13
+ * Wire FaceModule to the EventBridge.
14
+ * @param {import('../core/EventBridge.js').EventBridge} bridge
15
+ * @returns {Function[]} unsubscribe functions
16
+ */
17
+ export function connectFace(bridge) {
18
+ // FaceModule is a global loaded by index.html (legacy pattern)
19
+ const face = () => window.FaceModule;
20
+
21
+ const unsubs = [
22
+ bridge.on(AgentEvents.STATE_CHANGED, ({ state }) => {
23
+ if (!face()) return;
24
+ if (state === 'speaking') face().setMood('neutral');
25
+ if (state === 'listening') face().setMood('listening');
26
+ if (state === 'thinking') face().setMood('thinking');
27
+ if (state === 'idle') face().setMood('neutral');
28
+ }),
29
+ bridge.on(AgentEvents.MOOD, ({ mood }) => {
30
+ face()?.setMood(mood);
31
+ }),
32
+ bridge.on(AgentEvents.CONNECTED, () => {
33
+ face()?.setMood('happy');
34
+ }),
35
+ bridge.on(AgentEvents.DISCONNECTED, () => {
36
+ face()?.setMood('neutral');
37
+ }),
38
+ bridge.on(AgentEvents.ERROR, () => {
39
+ face()?.setMood('sad');
40
+ }),
41
+ ];
42
+
43
+ return unsubs;
44
+ }