openclaw-liveavatar 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/.next/BUILD_ID +1 -0
- package/.next/app-build-manifest.json +42 -0
- package/.next/app-path-routes-manifest.json +6 -0
- package/.next/build-manifest.json +33 -0
- package/.next/cache/.previewinfo +1 -0
- package/.next/cache/.rscinfo +1 -0
- package/.next/cache/.tsbuildinfo +1 -0
- package/.next/cache/chrome-devtools-workspace-uuid +1 -0
- package/.next/cache/next-devtools-config.json +1 -0
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/1.pack +0 -0
- package/.next/cache/webpack/client-production/2.pack +0 -0
- package/.next/cache/webpack/client-production/3.pack +0 -0
- package/.next/cache/webpack/client-production/4.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack.old +0 -0
- package/.next/cache/webpack/edge-server-production/0.pack +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/diagnostics/build-diagnostics.json +6 -0
- package/.next/diagnostics/framework.json +1 -0
- package/.next/export-marker.json +6 -0
- package/.next/images-manifest.json +58 -0
- package/.next/next-minimal-server.js.nft.json +1 -0
- package/.next/next-server.js.nft.json +1 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +61 -0
- package/.next/react-loadable-manifest.json +1 -0
- package/.next/required-server-files.json +320 -0
- package/.next/routes-manifest.json +53 -0
- package/.next/server/app/_not-found/page.js +5 -0
- package/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/.next/server/app/_not-found.html +4 -0
- package/.next/server/app/_not-found.meta +8 -0
- package/.next/server/app/_not-found.rsc +15 -0
- package/.next/server/app/api/get-avatars/route.js +1 -0
- package/.next/server/app/api/get-avatars/route.js.nft.json +1 -0
- package/.next/server/app/api/get-avatars/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/start-session/route.js +1 -0
- package/.next/server/app/api/start-session/route.js.nft.json +1 -0
- package/.next/server/app/api/start-session/route_client-reference-manifest.js +1 -0
- package/.next/server/app/index.html +4 -0
- package/.next/server/app/index.meta +7 -0
- package/.next/server/app/index.rsc +16 -0
- package/.next/server/app/page.js +9 -0
- package/.next/server/app/page.js.nft.json +1 -0
- package/.next/server/app/page_client-reference-manifest.js +1 -0
- package/.next/server/app-paths-manifest.json +6 -0
- package/.next/server/chunks/361.js +9 -0
- package/.next/server/chunks/611.js +6 -0
- package/.next/server/chunks/873.js +22 -0
- package/.next/server/functions-config-manifest.json +4 -0
- package/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/.next/server/middleware-build-manifest.js +1 -0
- package/.next/server/middleware-manifest.json +6 -0
- package/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/server/next-font-manifest.js +1 -0
- package/.next/server/next-font-manifest.json +1 -0
- package/.next/server/pages/404.html +4 -0
- package/.next/server/pages/500.html +1 -0
- package/.next/server/pages/_app.js +1 -0
- package/.next/server/pages/_app.js.nft.json +1 -0
- package/.next/server/pages/_document.js +1 -0
- package/.next/server/pages/_document.js.nft.json +1 -0
- package/.next/server/pages/_error.js +19 -0
- package/.next/server/pages/_error.js.nft.json +1 -0
- package/.next/server/pages-manifest.json +6 -0
- package/.next/server/server-reference-manifest.js +1 -0
- package/.next/server/server-reference-manifest.json +1 -0
- package/.next/server/webpack-runtime.js +1 -0
- package/.next/static/chunks/144d3bae-37bcc55d23f188ee.js +1 -0
- package/.next/static/chunks/255-35bf8c00c5dde345.js +1 -0
- package/.next/static/chunks/336-a66237a0a1db954a.js +1 -0
- package/.next/static/chunks/4bd1b696-c023c6e3521b1417.js +1 -0
- package/.next/static/chunks/app/_not-found/page-dfc6e5d8e6c6203c.js +1 -0
- package/.next/static/chunks/app/api/get-avatars/route-8017e1cff542d5d0.js +1 -0
- package/.next/static/chunks/app/api/start-session/route-8017e1cff542d5d0.js +1 -0
- package/.next/static/chunks/app/layout-ff675313cc8f8fcf.js +1 -0
- package/.next/static/chunks/app/page-9e4b703722bef650.js +1 -0
- package/.next/static/chunks/framework-de98b93a850cfc71.js +1 -0
- package/.next/static/chunks/main-1a0dcce460eb61ce.js +1 -0
- package/.next/static/chunks/main-app-e7f1007edc7ad7e1.js +1 -0
- package/.next/static/chunks/pages/_app-7d307437aca18ad4.js +1 -0
- package/.next/static/chunks/pages/_error-cb2a52f75f2162e2.js +1 -0
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/.next/static/chunks/webpack-4a462cecab786e93.js +1 -0
- package/.next/static/css/bfd73afa11897439.css +3 -0
- package/.next/static/v_GdCj8lVweDVhmIhhEcM/_buildManifest.js +1 -0
- package/.next/static/v_GdCj8lVweDVhmIhhEcM/_ssgManifest.js +1 -0
- package/.next/trace +2 -0
- package/.next/types/app/api/get-avatars/route.ts +347 -0
- package/.next/types/app/api/start-session/route.ts +347 -0
- package/.next/types/app/layout.ts +84 -0
- package/.next/types/app/page.ts +84 -0
- package/.next/types/cache-life.d.ts +141 -0
- package/.next/types/package.json +1 -0
- package/.next/types/routes.d.ts +74 -0
- package/.next/types/validator.ts +88 -0
- package/README.md +241 -0
- package/app/api/config.ts +18 -0
- package/app/api/get-avatars/route.ts +117 -0
- package/app/api/start-session/route.ts +95 -0
- package/app/globals.css +3 -0
- package/app/layout.tsx +37 -0
- package/app/page.tsx +9 -0
- package/bin/cli.js +100 -0
- package/package.json +66 -0
- package/src/components/LiveAvatarSession.tsx +825 -0
- package/src/components/OpenClawDemo.tsx +399 -0
- package/src/gateway/client.ts +522 -0
- package/src/gateway/types.ts +83 -0
- package/src/liveavatar/context.tsx +750 -0
- package/src/liveavatar/index.ts +6 -0
- package/src/liveavatar/types.ts +10 -0
- package/src/liveavatar/useAvatarActions.ts +41 -0
- package/src/liveavatar/useChatHistory.ts +7 -0
- package/src/liveavatar/useSession.ts +37 -0
- package/src/liveavatar/useTextChat.ts +32 -0
- package/src/liveavatar/useVoiceChat.ts +70 -0
- package/tsconfig.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# OpenClaw LiveAvatar
|
|
2
|
+
|
|
3
|
+
Give your OpenClaw agent a face and voice! Talk face-to-face with a real-time AI avatar powered by [LiveAvatar](https://liveavatar.com).
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Voice-to-Voice Conversation**: Speak naturally and hear your agent respond
|
|
10
|
+
- **Real-time Avatar**: Lip-synced video avatar with natural expressions
|
|
11
|
+
- **OpenClaw Integration**: Connects to your local OpenClaw Gateway
|
|
12
|
+
- **Smart TTS Summarization**: Long responses are summarized for natural speech
|
|
13
|
+
- **Echo Cancellation**: Won't respond to itself
|
|
14
|
+
- **Multiple Avatar Choices**: Select from custom or public avatars
|
|
15
|
+
- **Chat Transcript**: View the full conversation history
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
### Option 1: ClawHub Skill (Recommended)
|
|
20
|
+
|
|
21
|
+
If you have [OpenClaw](https://openclaw.ai) installed:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
clawhub install liveavatar
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then run the `/liveavatar` command in any OpenClaw chat.
|
|
28
|
+
|
|
29
|
+
### Option 2: NPX (Quick Start)
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Set your API key
|
|
33
|
+
export LIVEAVATAR_API_KEY=your_key_here
|
|
34
|
+
|
|
35
|
+
# Start OpenClaw Gateway (in another terminal)
|
|
36
|
+
openclaw gateway
|
|
37
|
+
|
|
38
|
+
# Run LiveAvatar
|
|
39
|
+
npx openclaw-liveavatar
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Option 3: Global Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install -g openclaw-liveavatar
|
|
46
|
+
|
|
47
|
+
# Then run anytime with:
|
|
48
|
+
openclaw-liveavatar
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Option 4: Development Setup
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
git clone https://github.com/eNNNo/openclaw-liveavatar.git
|
|
55
|
+
cd openclaw-liveavatar
|
|
56
|
+
npm install
|
|
57
|
+
cp .env.example .env.local
|
|
58
|
+
# Edit .env.local with your API key
|
|
59
|
+
npm run dev
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Prerequisites
|
|
63
|
+
|
|
64
|
+
- Node.js 18+
|
|
65
|
+
- [OpenClaw](https://openclaw.ai) installed with Gateway running
|
|
66
|
+
- [LiveAvatar API Key](https://app.liveavatar.com) (free tier available)
|
|
67
|
+
|
|
68
|
+
## Setup
|
|
69
|
+
|
|
70
|
+
### 1. Get Your API Key (Free)
|
|
71
|
+
|
|
72
|
+
1. Go to [app.liveavatar.com](https://app.liveavatar.com)
|
|
73
|
+
2. Create a free account
|
|
74
|
+
3. Copy your API key from the dashboard
|
|
75
|
+
|
|
76
|
+
### 2. Set Your API Key
|
|
77
|
+
|
|
78
|
+
**Option A: Environment variable**
|
|
79
|
+
```bash
|
|
80
|
+
export LIVEAVATAR_API_KEY=your_api_key_here
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Option B: OpenClaw config** (`~/.openclaw/openclaw.json`)
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"skills": {
|
|
87
|
+
"entries": {
|
|
88
|
+
"liveavatar": {
|
|
89
|
+
"env": {
|
|
90
|
+
"LIVEAVATAR_API_KEY": "your_api_key_here"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 3. Start OpenClaw Gateway
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
openclaw gateway
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 4. Launch LiveAvatar
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npx openclaw-liveavatar
|
|
108
|
+
# Or: /liveavatar (if installed as skill)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The interface will open at http://localhost:3001
|
|
112
|
+
|
|
113
|
+
> **Demo Mode**: If OpenClaw Gateway isn't running, the app will start in Demo Mode where you can interact with the avatar and learn about the integration.
|
|
114
|
+
|
|
115
|
+
## How It Works
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
|
119
|
+
│ You Speak │────▶│ LiveAvatar │────▶│ OpenClaw │
|
|
120
|
+
│ (Microphone) │ │ (Transcribe) │ │ Gateway │
|
|
121
|
+
└─────────────────┘ └──────────────────┘ └────────┬────────┘
|
|
122
|
+
│
|
|
123
|
+
┌─────────────────┐ ┌──────────────────┐ │
|
|
124
|
+
│ Avatar Speaks │◀────│ LiveAvatar │◀─────────────┘
|
|
125
|
+
│ (Lip-sync) │ │ (TTS + Video) │ Agent Response
|
|
126
|
+
└─────────────────┘ └──────────────────┘
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
1. **You speak** into your microphone
|
|
130
|
+
2. **LiveAvatar transcribes** your speech to text
|
|
131
|
+
3. **OpenClaw Gateway** receives the text and sends it to your agent
|
|
132
|
+
4. **Your agent responds** with text
|
|
133
|
+
5. **LiveAvatar synthesizes** the response as speech
|
|
134
|
+
6. **The avatar speaks** with synchronized lip movements
|
|
135
|
+
|
|
136
|
+
## Configuration
|
|
137
|
+
|
|
138
|
+
### Environment Variables
|
|
139
|
+
|
|
140
|
+
| Variable | Description | Default |
|
|
141
|
+
|----------|-------------|---------|
|
|
142
|
+
| `LIVEAVATAR_API_KEY` | Your LiveAvatar API key (required) | - |
|
|
143
|
+
| `OPENCLAW_GATEWAY_URL` | WebSocket URL for OpenClaw Gateway | `ws://127.0.0.1:18789` |
|
|
144
|
+
| `OPENCLAW_GATEWAY_TOKEN` | Token for remote Gateway access | - |
|
|
145
|
+
|
|
146
|
+
### OpenClaw Skill Installation
|
|
147
|
+
|
|
148
|
+
This can also be installed as an OpenClaw skill:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
clawhub install liveavatar
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Or invoke directly:
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
/liveavatar
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Development
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# Install dependencies
|
|
164
|
+
npm install
|
|
165
|
+
|
|
166
|
+
# Start development server
|
|
167
|
+
npm run dev
|
|
168
|
+
|
|
169
|
+
# Build for production
|
|
170
|
+
npm run build
|
|
171
|
+
|
|
172
|
+
# Start production server
|
|
173
|
+
npm start
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Architecture
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
openclaw-liveavatar/
|
|
180
|
+
├── app/ # Next.js app directory
|
|
181
|
+
│ ├── api/ # API routes
|
|
182
|
+
│ │ ├── config.ts # Configuration
|
|
183
|
+
│ │ ├── start-session/ # Session token generation
|
|
184
|
+
│ │ └── get-avatars/ # Avatar listing
|
|
185
|
+
│ ├── layout.tsx # Root layout
|
|
186
|
+
│ └── page.tsx # Main page
|
|
187
|
+
├── src/
|
|
188
|
+
│ ├── components/ # React components
|
|
189
|
+
│ │ ├── OpenClawDemo.tsx # Setup/landing UI
|
|
190
|
+
│ │ └── LiveAvatarSession.tsx # Session UI
|
|
191
|
+
│ ├── gateway/ # OpenClaw Gateway client
|
|
192
|
+
│ │ ├── client.ts # WebSocket client
|
|
193
|
+
│ │ └── types.ts # Protocol types
|
|
194
|
+
│ └── liveavatar/ # LiveAvatar SDK hooks
|
|
195
|
+
│ ├── context.tsx # React context with Gateway bridge
|
|
196
|
+
│ ├── useSession.ts # Session management
|
|
197
|
+
│ ├── useVoiceChat.ts # Voice chat controls
|
|
198
|
+
│ └── ...
|
|
199
|
+
├── skills/ # OpenClaw skill definition
|
|
200
|
+
│ └── liveavatar/
|
|
201
|
+
│ └── SKILL.md
|
|
202
|
+
└── openclaw.plugin.json # Channel plugin manifest
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Troubleshooting
|
|
206
|
+
|
|
207
|
+
### "OpenClaw Disconnected"
|
|
208
|
+
|
|
209
|
+
Make sure your OpenClaw Gateway is running:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
openclaw gateway
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### "No avatars available"
|
|
216
|
+
|
|
217
|
+
Verify your `LIVEAVATAR_API_KEY` is set correctly in `.env.local`
|
|
218
|
+
|
|
219
|
+
### Avatar not responding to speech
|
|
220
|
+
|
|
221
|
+
1. Check microphone permissions in your browser
|
|
222
|
+
2. Ensure the mic is not muted (green button = active)
|
|
223
|
+
3. Verify Gateway shows "Connected" status
|
|
224
|
+
4. Try a different microphone from the dropdown
|
|
225
|
+
|
|
226
|
+
### Connection quality issues
|
|
227
|
+
|
|
228
|
+
LiveAvatar uses WebRTC. For best results:
|
|
229
|
+
- Use a stable internet connection
|
|
230
|
+
- Close other video/audio applications
|
|
231
|
+
- Use Chrome or Edge for best WebRTC support
|
|
232
|
+
|
|
233
|
+
## Credits
|
|
234
|
+
|
|
235
|
+
- [HeyGen LiveAvatar](https://liveavatar.com) - Real-time AI avatar technology
|
|
236
|
+
- [OpenClaw](https://openclaw.ai) - AI agent gateway
|
|
237
|
+
- Based on [liveavatar-ai-sdr](https://github.com/eNNNo/liveavatar-ai-sdr)
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
MIT
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// LiveAvatar Configuration
|
|
2
|
+
// Get your free API key from https://app.liveavatar.com/developers
|
|
3
|
+
export const LIVEAVATAR_API_KEY = process.env.LIVEAVATAR_API_KEY || "";
|
|
4
|
+
export const LIVEAVATAR_API_URL = "https://api.liveavatar.com";
|
|
5
|
+
|
|
6
|
+
// Default avatar and voice (can be overridden by user selection)
|
|
7
|
+
export const DEFAULT_AVATAR_ID = "1c690fe7-23e0-49f9-bfba-14344450285b"; // Stock avatar
|
|
8
|
+
export const DEFAULT_VOICE_ID = ""; // Will use avatar's default voice
|
|
9
|
+
|
|
10
|
+
// Sandbox mode for development (uses minimal credits)
|
|
11
|
+
// Note: Not all avatars support sandbox mode - set to false for full avatar access
|
|
12
|
+
export const IS_SANDBOX = false;
|
|
13
|
+
|
|
14
|
+
export const LANGUAGE = "en";
|
|
15
|
+
|
|
16
|
+
// OpenClaw Gateway Configuration
|
|
17
|
+
export const OPENCLAW_GATEWAY_URL = process.env.OPENCLAW_GATEWAY_URL || "ws://127.0.0.1:18789";
|
|
18
|
+
export const OPENCLAW_GATEWAY_TOKEN = process.env.OPENCLAW_GATEWAY_TOKEN || "";
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { LIVEAVATAR_API_KEY, LIVEAVATAR_API_URL } from "../config";
|
|
2
|
+
|
|
3
|
+
interface Avatar {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
preview_url?: string;
|
|
7
|
+
status?: string;
|
|
8
|
+
is_custom?: boolean;
|
|
9
|
+
is_expired?: boolean;
|
|
10
|
+
default_voice?: {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface AvatarResponse {
|
|
17
|
+
data?: {
|
|
18
|
+
results?: Avatar[];
|
|
19
|
+
total?: number;
|
|
20
|
+
page?: number;
|
|
21
|
+
limit?: number;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Fetch all pages from a paginated endpoint
|
|
26
|
+
async function fetchAllAvatars(endpoint: string): Promise<Avatar[]> {
|
|
27
|
+
const allAvatars: Avatar[] = [];
|
|
28
|
+
let page = 1;
|
|
29
|
+
const limit = 100;
|
|
30
|
+
|
|
31
|
+
while (true) {
|
|
32
|
+
const res = await fetch(`${LIVEAVATAR_API_URL}${endpoint}?page=${page}&limit=${limit}`, {
|
|
33
|
+
headers: {
|
|
34
|
+
"X-API-KEY": LIVEAVATAR_API_KEY!,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
console.error(`Failed to fetch ${endpoint} page ${page}:`, res.status);
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const data: AvatarResponse = await res.json();
|
|
44
|
+
const pageAvatars = data.data?.results || [];
|
|
45
|
+
allAvatars.push(...pageAvatars);
|
|
46
|
+
|
|
47
|
+
const total = data.data?.total || pageAvatars.length;
|
|
48
|
+
console.log(`[Avatars] ${endpoint} page ${page}: ${pageAvatars.length} avatars (total: ${total})`);
|
|
49
|
+
|
|
50
|
+
// Check if we've fetched all
|
|
51
|
+
if (allAvatars.length >= total || pageAvatars.length === 0) {
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
page++;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return allAvatars;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function GET() {
|
|
61
|
+
if (!LIVEAVATAR_API_KEY) {
|
|
62
|
+
return new Response(
|
|
63
|
+
JSON.stringify({
|
|
64
|
+
error: "LiveAvatar API key not configured",
|
|
65
|
+
customAvatars: [],
|
|
66
|
+
publicAvatars: [],
|
|
67
|
+
}),
|
|
68
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Fetch both custom (user) avatars and public avatars in parallel
|
|
74
|
+
const [customAvatars, publicAvatars] = await Promise.all([
|
|
75
|
+
fetchAllAvatars("/v1/avatars"), // User's custom avatars
|
|
76
|
+
fetchAllAvatars("/v1/avatars/public"), // Public avatars
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
// Mark custom avatars
|
|
80
|
+
const markedCustomAvatars = customAvatars.map(avatar => ({
|
|
81
|
+
...avatar,
|
|
82
|
+
is_custom: true,
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
// Mark public avatars (they don't expire)
|
|
86
|
+
const markedPublicAvatars = publicAvatars.map(avatar => ({
|
|
87
|
+
...avatar,
|
|
88
|
+
is_custom: false,
|
|
89
|
+
is_expired: false, // Public avatars don't expire
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
console.log(`[Avatars] Total: ${markedCustomAvatars.length} custom, ${markedPublicAvatars.length} public`);
|
|
93
|
+
|
|
94
|
+
return new Response(
|
|
95
|
+
JSON.stringify({
|
|
96
|
+
customAvatars: markedCustomAvatars,
|
|
97
|
+
publicAvatars: markedPublicAvatars,
|
|
98
|
+
}),
|
|
99
|
+
{
|
|
100
|
+
status: 200,
|
|
101
|
+
headers: { "Content-Type": "application/json" },
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error("Error fetching avatars:", error);
|
|
106
|
+
return new Response(
|
|
107
|
+
JSON.stringify({
|
|
108
|
+
customAvatars: [],
|
|
109
|
+
publicAvatars: [],
|
|
110
|
+
}),
|
|
111
|
+
{
|
|
112
|
+
status: 200,
|
|
113
|
+
headers: { "Content-Type": "application/json" },
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LIVEAVATAR_API_KEY,
|
|
3
|
+
LIVEAVATAR_API_URL,
|
|
4
|
+
DEFAULT_AVATAR_ID,
|
|
5
|
+
DEFAULT_VOICE_ID,
|
|
6
|
+
IS_SANDBOX,
|
|
7
|
+
LANGUAGE,
|
|
8
|
+
} from "../config";
|
|
9
|
+
|
|
10
|
+
export async function POST(request: Request) {
|
|
11
|
+
let session_token = "";
|
|
12
|
+
let session_id = "";
|
|
13
|
+
|
|
14
|
+
// Allow dynamic parameters from request body, fall back to defaults
|
|
15
|
+
let avatarId = DEFAULT_AVATAR_ID;
|
|
16
|
+
let voiceId = DEFAULT_VOICE_ID;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const body = await request.json();
|
|
20
|
+
if (body.avatarId) {
|
|
21
|
+
avatarId = body.avatarId;
|
|
22
|
+
}
|
|
23
|
+
if (body.voiceId) {
|
|
24
|
+
voiceId = body.voiceId;
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// No body or invalid JSON, use defaults
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check for API key
|
|
31
|
+
if (!LIVEAVATAR_API_KEY) {
|
|
32
|
+
return new Response(
|
|
33
|
+
JSON.stringify({
|
|
34
|
+
error:
|
|
35
|
+
"LiveAvatar API key not configured. Please set LIVEAVATAR_API_KEY in your .env.local file.",
|
|
36
|
+
}),
|
|
37
|
+
{ status: 500, headers: { "Content-Type": "application/json" } }
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// For OpenClaw integration, we use FULL mode but without a predefined context
|
|
43
|
+
// The avatar will act as a passthrough - we'll control what it says via the bridge
|
|
44
|
+
const res = await fetch(`${LIVEAVATAR_API_URL}/v1/sessions/token`, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: {
|
|
47
|
+
"X-API-KEY": LIVEAVATAR_API_KEY,
|
|
48
|
+
"Content-Type": "application/json",
|
|
49
|
+
},
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
mode: "FULL",
|
|
52
|
+
avatar_id: avatarId,
|
|
53
|
+
avatar_persona: {
|
|
54
|
+
// Only include voice_id if provided
|
|
55
|
+
...(voiceId ? { voice_id: voiceId } : {}),
|
|
56
|
+
// No context_id - we'll use OpenClaw for the intelligence
|
|
57
|
+
language: LANGUAGE,
|
|
58
|
+
},
|
|
59
|
+
is_sandbox: IS_SANDBOX,
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
const resp = await res.json();
|
|
65
|
+
const errorMessage =
|
|
66
|
+
resp.data?.[0]?.message ?? "Failed to retrieve session token";
|
|
67
|
+
return new Response(JSON.stringify({ error: errorMessage }), {
|
|
68
|
+
status: res.status,
|
|
69
|
+
headers: { "Content-Type": "application/json" },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const data = await res.json();
|
|
74
|
+
session_token = data.data.session_token;
|
|
75
|
+
session_id = data.data.session_id;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error("Error retrieving session token:", error);
|
|
78
|
+
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
|
79
|
+
status: 500,
|
|
80
|
+
headers: { "Content-Type": "application/json" },
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!session_token) {
|
|
85
|
+
return new Response(
|
|
86
|
+
JSON.stringify({ error: "Failed to retrieve session token" }),
|
|
87
|
+
{ status: 500, headers: { "Content-Type": "application/json" } }
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return new Response(JSON.stringify({ session_token, session_id }), {
|
|
92
|
+
status: 200,
|
|
93
|
+
headers: { "Content-Type": "application/json" },
|
|
94
|
+
});
|
|
95
|
+
}
|
package/app/globals.css
ADDED
package/app/layout.tsx
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import "./globals.css";
|
|
2
|
+
import type { Metadata } from "next";
|
|
3
|
+
|
|
4
|
+
export const metadata: Metadata = {
|
|
5
|
+
title: "OpenClaw LiveAvatar",
|
|
6
|
+
description: "Talk to your OpenClaw agent face-to-face with a real-time AI avatar",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// Gateway configuration from environment variables
|
|
10
|
+
const gatewayConfig = {
|
|
11
|
+
url: process.env.OPENCLAW_GATEWAY_URL || "ws://127.0.0.1:18789",
|
|
12
|
+
token: process.env.OPENCLAW_GATEWAY_TOKEN || "",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default function RootLayout({
|
|
16
|
+
children,
|
|
17
|
+
}: {
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
}) {
|
|
20
|
+
return (
|
|
21
|
+
<html lang="en">
|
|
22
|
+
<head>
|
|
23
|
+
<script
|
|
24
|
+
dangerouslySetInnerHTML={{
|
|
25
|
+
__html: `
|
|
26
|
+
window.__OPENCLAW_GATEWAY_URL = ${JSON.stringify(gatewayConfig.url)};
|
|
27
|
+
window.__OPENCLAW_GATEWAY_TOKEN = ${JSON.stringify(gatewayConfig.token)};
|
|
28
|
+
`,
|
|
29
|
+
}}
|
|
30
|
+
/>
|
|
31
|
+
</head>
|
|
32
|
+
<body className="bg-gray-950 text-white min-h-screen">
|
|
33
|
+
{children}
|
|
34
|
+
</body>
|
|
35
|
+
</html>
|
|
36
|
+
);
|
|
37
|
+
}
|
package/app/page.tsx
ADDED
package/bin/cli.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname, join } from 'path';
|
|
6
|
+
import open from 'open';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
const projectRoot = join(__dirname, '..');
|
|
11
|
+
|
|
12
|
+
const PORT = process.env.PORT || 3001;
|
|
13
|
+
|
|
14
|
+
console.log('');
|
|
15
|
+
console.log(' \x1b[38;5;208m\x1b[1m🦞 OpenClaw LiveAvatar\x1b[0m');
|
|
16
|
+
console.log(' ─────────────────────────────────────');
|
|
17
|
+
console.log('');
|
|
18
|
+
|
|
19
|
+
// Check for LIVEAVATAR_API_KEY
|
|
20
|
+
if (!process.env.LIVEAVATAR_API_KEY) {
|
|
21
|
+
console.log(' \x1b[33m⚠️ Warning: LIVEAVATAR_API_KEY not set\x1b[0m');
|
|
22
|
+
console.log('');
|
|
23
|
+
console.log(' To use LiveAvatar, you need an API key:');
|
|
24
|
+
console.log(' 1. Get your free key at \x1b[36mhttps://app.liveavatar.com\x1b[0m');
|
|
25
|
+
console.log(' 2. Set it: \x1b[90mexport LIVEAVATAR_API_KEY=your_key_here\x1b[0m');
|
|
26
|
+
console.log('');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log(` Starting server on port ${PORT}...`);
|
|
30
|
+
console.log('');
|
|
31
|
+
|
|
32
|
+
// Start the Next.js server
|
|
33
|
+
const server = spawn('npx', ['next', 'start', '-p', String(PORT)], {
|
|
34
|
+
cwd: projectRoot,
|
|
35
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
36
|
+
env: { ...process.env, PORT: String(PORT) }
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
let serverReady = false;
|
|
40
|
+
|
|
41
|
+
server.stdout.on('data', (data) => {
|
|
42
|
+
const output = data.toString();
|
|
43
|
+
|
|
44
|
+
// Detect when server is ready
|
|
45
|
+
if (!serverReady && (output.includes('Ready') || output.includes('started') || output.includes('localhost'))) {
|
|
46
|
+
serverReady = true;
|
|
47
|
+
const url = `http://localhost:${PORT}`;
|
|
48
|
+
|
|
49
|
+
console.log(` \x1b[32m✓ Server ready!\x1b[0m`);
|
|
50
|
+
console.log('');
|
|
51
|
+
console.log(` \x1b[1mOpen in browser:\x1b[0m \x1b[36m${url}\x1b[0m`);
|
|
52
|
+
console.log('');
|
|
53
|
+
console.log(' \x1b[90mThe LiveAvatar interface will connect to your\x1b[0m');
|
|
54
|
+
console.log(' \x1b[90mOpenClaw Gateway on port 18789 automatically.\x1b[0m');
|
|
55
|
+
console.log('');
|
|
56
|
+
console.log(' \x1b[90mPress Ctrl+C to stop\x1b[0m');
|
|
57
|
+
console.log('');
|
|
58
|
+
|
|
59
|
+
// Open browser
|
|
60
|
+
open(url).catch(() => {
|
|
61
|
+
console.log(' \x1b[90m(Could not open browser automatically)\x1b[0m');
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
server.stderr.on('data', (data) => {
|
|
67
|
+
const output = data.toString();
|
|
68
|
+
// Filter out noisy Next.js output
|
|
69
|
+
if (!output.includes('Compiling') && !output.includes('compiled')) {
|
|
70
|
+
process.stderr.write(data);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
server.on('error', (err) => {
|
|
75
|
+
console.error(' \x1b[31m✗ Failed to start server:\x1b[0m', err.message);
|
|
76
|
+
console.log('');
|
|
77
|
+
console.log(' Make sure you have run:');
|
|
78
|
+
console.log(' \x1b[90mnpm install\x1b[0m');
|
|
79
|
+
console.log('');
|
|
80
|
+
process.exit(1);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
server.on('close', (code) => {
|
|
84
|
+
if (code !== 0 && code !== null) {
|
|
85
|
+
console.log('');
|
|
86
|
+
console.log(` \x1b[31mServer exited with code ${code}\x1b[0m`);
|
|
87
|
+
}
|
|
88
|
+
process.exit(code || 0);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Handle graceful shutdown
|
|
92
|
+
process.on('SIGINT', () => {
|
|
93
|
+
console.log('');
|
|
94
|
+
console.log(' \x1b[90mShutting down...\x1b[0m');
|
|
95
|
+
server.kill('SIGINT');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
process.on('SIGTERM', () => {
|
|
99
|
+
server.kill('SIGTERM');
|
|
100
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-liveavatar",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Real-time video avatar chat interface for OpenClaw - talk face-to-face with your AI agent",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"openclaw-liveavatar": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "next dev -p 3001 --turbopack",
|
|
11
|
+
"build": "next build",
|
|
12
|
+
"start": "next start -p 3001",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"openclaw",
|
|
17
|
+
"liveavatar",
|
|
18
|
+
"heygen",
|
|
19
|
+
"ai",
|
|
20
|
+
"avatar",
|
|
21
|
+
"voice",
|
|
22
|
+
"chat",
|
|
23
|
+
"video",
|
|
24
|
+
"assistant"
|
|
25
|
+
],
|
|
26
|
+
"author": "eNNNo",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/eNNNo/openclaw-liveavatar.git"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/eNNNo/openclaw-liveavatar/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/eNNNo/openclaw-liveavatar#readme",
|
|
36
|
+
"files": [
|
|
37
|
+
"bin",
|
|
38
|
+
"app",
|
|
39
|
+
"src",
|
|
40
|
+
"public",
|
|
41
|
+
".next",
|
|
42
|
+
"next.config.ts",
|
|
43
|
+
"tailwind.config.ts",
|
|
44
|
+
"postcss.config.mjs",
|
|
45
|
+
"tsconfig.json"
|
|
46
|
+
],
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@heygen/liveavatar-web-sdk": "^0.0.10",
|
|
49
|
+
"next": "^15.4.2",
|
|
50
|
+
"open": "^10.1.0",
|
|
51
|
+
"react": "^19.1.0",
|
|
52
|
+
"react-dom": "^19.1.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/node": "22.14.0",
|
|
56
|
+
"@types/react": "19.1.6",
|
|
57
|
+
"@types/react-dom": "19.1.6",
|
|
58
|
+
"autoprefixer": "^10.4.21",
|
|
59
|
+
"postcss": "^8.5.3",
|
|
60
|
+
"tailwindcss": "^3.4.17",
|
|
61
|
+
"typescript": "5.8.2"
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=18.0.0"
|
|
65
|
+
}
|
|
66
|
+
}
|