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.
- package/.env.example +104 -0
- package/Dockerfile +30 -0
- package/LICENSE +21 -0
- package/README.md +638 -0
- package/SETUP.md +360 -0
- package/app.py +232 -0
- package/auto-approve-devices.js +111 -0
- package/cli/index.js +372 -0
- package/config/__init__.py +4 -0
- package/config/default.yaml +43 -0
- package/config/flags.yaml +67 -0
- package/config/loader.py +203 -0
- package/config/providers.yaml +71 -0
- package/config/speech_normalization.yaml +182 -0
- package/config/theme.json +4 -0
- package/data/greetings.json +25 -0
- package/default-pages/ai-image-creator.html +915 -0
- package/default-pages/bulk-image-uploader.html +492 -0
- package/default-pages/desktop.html +2865 -0
- package/default-pages/file-explorer.html +854 -0
- package/default-pages/interactive-map.html +655 -0
- package/default-pages/style-guide.html +1005 -0
- package/default-pages/website-setup.html +1623 -0
- package/deploy/openclaw/Dockerfile +46 -0
- package/deploy/openvoiceui.service +30 -0
- package/deploy/setup-nginx.sh +50 -0
- package/deploy/setup-sudo.sh +306 -0
- package/deploy/skill-runner/Dockerfile +19 -0
- package/deploy/skill-runner/requirements.txt +14 -0
- package/deploy/skill-runner/server.py +269 -0
- package/deploy/supertonic/Dockerfile +22 -0
- package/deploy/supertonic/server.py +79 -0
- package/docker-compose.pinokio.yml +11 -0
- package/docker-compose.yml +59 -0
- package/greetings.json +25 -0
- package/index.html +65 -0
- package/inject-device-identity.js +142 -0
- package/package.json +82 -0
- package/profiles/default.json +114 -0
- package/profiles/manager.py +354 -0
- package/profiles/schema.json +337 -0
- package/prompts/voice-system-prompt.md +149 -0
- package/providers/__init__.py +39 -0
- package/providers/base.py +63 -0
- package/providers/llm/__init__.py +12 -0
- package/providers/llm/base.py +71 -0
- package/providers/llm/clawdbot_provider.py +112 -0
- package/providers/llm/zai_provider.py +115 -0
- package/providers/registry.py +320 -0
- package/providers/stt/__init__.py +12 -0
- package/providers/stt/base.py +58 -0
- package/providers/stt/webspeech_provider.py +49 -0
- package/providers/stt/whisper_provider.py +100 -0
- package/providers/tts/__init__.py +20 -0
- package/providers/tts/base.py +91 -0
- package/providers/tts/groq_provider.py +74 -0
- package/providers/tts/supertonic_provider.py +72 -0
- package/requirements.txt +38 -0
- package/routes/__init__.py +10 -0
- package/routes/admin.py +515 -0
- package/routes/canvas.py +1315 -0
- package/routes/chat.py +51 -0
- package/routes/conversation.py +2158 -0
- package/routes/elevenlabs_hybrid.py +306 -0
- package/routes/greetings.py +98 -0
- package/routes/icons.py +279 -0
- package/routes/image_gen.py +364 -0
- package/routes/instructions.py +190 -0
- package/routes/music.py +838 -0
- package/routes/onboarding.py +43 -0
- package/routes/pi.py +62 -0
- package/routes/profiles.py +215 -0
- package/routes/report_issue.py +68 -0
- package/routes/static_files.py +533 -0
- package/routes/suno.py +664 -0
- package/routes/theme.py +81 -0
- package/routes/transcripts.py +199 -0
- package/routes/vision.py +348 -0
- package/routes/workspace.py +288 -0
- package/server.py +1510 -0
- package/services/__init__.py +1 -0
- package/services/auth.py +143 -0
- package/services/canvas_versioning.py +239 -0
- package/services/db_pool.py +107 -0
- package/services/gateway.py +16 -0
- package/services/gateway_manager.py +333 -0
- package/services/gateways/__init__.py +12 -0
- package/services/gateways/base.py +110 -0
- package/services/gateways/compat.py +264 -0
- package/services/gateways/openclaw.py +1134 -0
- package/services/health.py +100 -0
- package/services/memory_client.py +455 -0
- package/services/paths.py +26 -0
- package/services/speech_normalizer.py +285 -0
- package/services/tts.py +270 -0
- package/setup-config.js +262 -0
- package/sounds/air_horn.mp3 +0 -0
- package/sounds/bruh.mp3 +0 -0
- package/sounds/crowd_cheer.mp3 +0 -0
- package/sounds/gunshot.mp3 +0 -0
- package/sounds/impact.mp3 +0 -0
- package/sounds/lets_go.mp3 +0 -0
- package/sounds/record_stop.mp3 +0 -0
- package/sounds/rewind.mp3 +0 -0
- package/sounds/sad_trombone.mp3 +0 -0
- package/sounds/scratch_long.mp3 +0 -0
- package/sounds/yeah.mp3 +0 -0
- package/src/adapters/ClawdBotAdapter.js +264 -0
- package/src/adapters/_template.js +133 -0
- package/src/adapters/elevenlabs-classic.js +841 -0
- package/src/adapters/elevenlabs-hybrid.js +812 -0
- package/src/adapters/hume-evi.js +676 -0
- package/src/admin.html +1339 -0
- package/src/app.js +8802 -0
- package/src/core/Config.js +173 -0
- package/src/core/EmotionEngine.js +307 -0
- package/src/core/EventBridge.js +180 -0
- package/src/core/EventBus.js +117 -0
- package/src/core/VoiceSession.js +607 -0
- package/src/face/BaseFace.js +259 -0
- package/src/face/EyeFace.js +208 -0
- package/src/face/HaloSmokeFace.js +509 -0
- package/src/face/manifest.json +27 -0
- package/src/face/previews/eyes.svg +16 -0
- package/src/face/previews/orb.svg +29 -0
- package/src/features/MusicPlayer.js +620 -0
- package/src/features/Soundboard.js +128 -0
- package/src/providers/DeepgramSTT.js +472 -0
- package/src/providers/DeepgramStreamingSTT.js +766 -0
- package/src/providers/GroqSTT.js +559 -0
- package/src/providers/TTSPlayer.js +323 -0
- package/src/providers/WebSpeechSTT.js +479 -0
- package/src/providers/tts/BaseTTSProvider.js +81 -0
- package/src/providers/tts/HumeProvider.js +77 -0
- package/src/providers/tts/SupertonicProvider.js +174 -0
- package/src/providers/tts/index.js +140 -0
- package/src/shell/adapter-registry.js +154 -0
- package/src/shell/caller-bridge.js +35 -0
- package/src/shell/camera-bridge.js +28 -0
- package/src/shell/canvas-bridge.js +32 -0
- package/src/shell/commercial-bridge.js +44 -0
- package/src/shell/face-bridge.js +44 -0
- package/src/shell/music-bridge.js +60 -0
- package/src/shell/orchestrator.js +233 -0
- package/src/shell/profile-discovery.js +303 -0
- package/src/shell/sounds-bridge.js +28 -0
- package/src/shell/transcript-bridge.js +61 -0
- package/src/shell/waveform-bridge.js +33 -0
- package/src/styles/base.css +2862 -0
- package/src/styles/face.css +417 -0
- package/src/styles/pi-overrides.css +89 -0
- package/src/styles/theme-dark.css +67 -0
- package/src/test-tts.html +175 -0
- package/src/ui/AppShell.js +544 -0
- package/src/ui/ProfileSwitcher.js +228 -0
- package/src/ui/SessionControl.js +240 -0
- package/src/ui/face/FacePicker.js +195 -0
- package/src/ui/face/FaceRenderer.js +309 -0
- package/src/ui/settings/PlaylistEditor.js +366 -0
- package/src/ui/settings/SettingsPanel.css +684 -0
- package/src/ui/settings/SettingsPanel.js +419 -0
- package/src/ui/settings/TTSVoicePreview.js +210 -0
- package/src/ui/themes/ThemeManager.js +213 -0
- package/src/ui/visualizers/BaseVisualizer.js +29 -0
- package/src/ui/visualizers/PartyFXVisualizer.css +291 -0
- package/src/ui/visualizers/PartyFXVisualizer.js +637 -0
- package/static/emulators/jsdos/js-dos.css +1 -0
- package/static/emulators/jsdos/js-dos.js +22 -0
- package/static/favicon.svg +55 -0
- package/static/icons/apple-touch-icon.png +0 -0
- package/static/icons/favicon-32.png +0 -0
- package/static/icons/icon-192.png +0 -0
- package/static/icons/icon-512.png +0 -0
- package/static/install.html +449 -0
- package/static/manifest.json +26 -0
- package/static/sw.js +21 -0
- package/tts_providers/__init__.py +136 -0
- package/tts_providers/base_provider.py +319 -0
- package/tts_providers/groq_provider.py +155 -0
- package/tts_providers/hume_provider.py +226 -0
- package/tts_providers/providers_config.json +119 -0
- package/tts_providers/qwen3_provider.py +371 -0
- package/tts_providers/resemble_provider.py +315 -0
- package/tts_providers/supertonic_provider.py +557 -0
- 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
|
+
}
|