openpersona 0.3.0 → 0.4.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/README.md +22 -20
- package/bin/cli.js +4 -4
- package/layers/faculties/music/SKILL.md +54 -62
- package/layers/faculties/music/faculty.json +3 -3
- package/layers/faculties/music/scripts/compose.js +179 -138
- package/layers/faculties/music/scripts/compose.sh +119 -154
- package/layers/faculties/selfie/faculty.json +1 -1
- package/layers/faculties/voice/SKILL.md +10 -8
- package/layers/faculties/voice/faculty.json +2 -2
- package/layers/soul/README.md +31 -4
- package/layers/soul/constitution.md +136 -0
- package/lib/contributor.js +22 -14
- package/lib/downloader.js +6 -1
- package/lib/generator.js +47 -11
- package/lib/installer.js +10 -0
- package/lib/publisher/clawhub.js +4 -3
- package/lib/utils.js +19 -0
- package/package.json +7 -7
- package/presets/ai-girlfriend/manifest.json +2 -3
- package/presets/health-butler/manifest.json +1 -1
- package/presets/life-assistant/manifest.json +1 -1
- package/presets/samantha/manifest.json +2 -3
- package/skills/open-persona/SKILL.md +125 -0
- package/skills/open-persona/references/CONTRIBUTE.md +38 -0
- package/skills/open-persona/references/FACULTIES.md +26 -0
- package/skills/open-persona/references/HEARTBEAT.md +35 -0
- package/templates/skill.template.md +9 -1
- package/templates/soul-injection.template.md +30 -3
- package/layers/faculties/soul-evolution/SKILL.md +0 -41
- package/layers/faculties/soul-evolution/faculty.json +0 -9
- package/skill/SKILL.md +0 -209
- /package/layers/{faculties/soul-evolution → soul}/soul-state.template.json +0 -0
|
@@ -1,45 +1,46 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* OpenPersona Music Faculty —
|
|
3
|
+
* OpenPersona Music Faculty — ElevenLabs Music API (music_v1)
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
6
|
* node compose.js "a soft piano piece about starlight"
|
|
7
|
-
* node compose.js "
|
|
8
|
-
* node compose.js "
|
|
9
|
-
* node compose.js "upbeat pop" --output ./song.mp3
|
|
7
|
+
* node compose.js "dreamy lo-fi beats" --instrumental
|
|
8
|
+
* node compose.js "indie folk ballad" --plan
|
|
9
|
+
* node compose.js "upbeat pop" --output ./song.mp3 --duration 60
|
|
10
10
|
*
|
|
11
11
|
* Environment:
|
|
12
|
-
*
|
|
13
|
-
* SUNO_MODEL - Default model (V4, V4_5, V4_5PLUS, V4_5ALL, V5)
|
|
12
|
+
* ELEVENLABS_API_KEY - ElevenLabs API key (shared with voice faculty)
|
|
14
13
|
*/
|
|
15
14
|
|
|
16
15
|
const https = require('https');
|
|
17
|
-
const http = require('http');
|
|
18
16
|
const fs = require('fs');
|
|
19
17
|
const path = require('path');
|
|
20
18
|
|
|
21
|
-
const API_BASE = 'https://api.
|
|
19
|
+
const API_BASE = 'https://api.elevenlabs.io';
|
|
20
|
+
const DEFAULT_FORMAT = 'mp3_44100_128';
|
|
22
21
|
|
|
23
22
|
// --- Argument parsing ---
|
|
24
23
|
function parseArgs(args) {
|
|
25
24
|
const opts = {
|
|
26
25
|
prompt: '',
|
|
27
|
-
style: '',
|
|
28
|
-
title: '',
|
|
29
26
|
instrumental: false,
|
|
30
|
-
|
|
27
|
+
plan: false,
|
|
28
|
+
duration: null, // seconds; null = let model decide
|
|
29
|
+
format: DEFAULT_FORMAT,
|
|
31
30
|
output: null,
|
|
32
|
-
|
|
31
|
+
channel: '',
|
|
32
|
+
caption: '',
|
|
33
33
|
};
|
|
34
34
|
let i = 0;
|
|
35
35
|
while (i < args.length) {
|
|
36
36
|
switch (args[i]) {
|
|
37
|
-
case '--style': opts.style = args[++i]; break;
|
|
38
|
-
case '--title': opts.title = args[++i]; break;
|
|
39
37
|
case '--instrumental': opts.instrumental = true; break;
|
|
40
|
-
case '--
|
|
38
|
+
case '--plan': opts.plan = true; break;
|
|
39
|
+
case '--duration': opts.duration = parseInt(args[++i], 10); break;
|
|
40
|
+
case '--format': opts.format = args[++i]; break;
|
|
41
41
|
case '--output': opts.output = args[++i]; break;
|
|
42
|
-
case '--
|
|
42
|
+
case '--channel': opts.channel = args[++i]; break;
|
|
43
|
+
case '--caption': opts.caption = args[++i]; break;
|
|
43
44
|
case '--help':
|
|
44
45
|
case '-h':
|
|
45
46
|
printUsage();
|
|
@@ -58,26 +59,48 @@ function printUsage() {
|
|
|
58
59
|
Usage: node compose.js <prompt> [options]
|
|
59
60
|
|
|
60
61
|
Options:
|
|
61
|
-
--style <style> Music style/genre (enables custom mode)
|
|
62
|
-
--title <title> Song title (enables custom mode)
|
|
63
62
|
--instrumental Generate instrumental only (no vocals)
|
|
64
|
-
--
|
|
65
|
-
--
|
|
66
|
-
--
|
|
63
|
+
--plan Use composition plan mode (structured sections)
|
|
64
|
+
--duration <secs> Song length in seconds (3-600, default: auto)
|
|
65
|
+
--format <format> Output format (default: mp3_44100_128)
|
|
66
|
+
--output <path> Save audio to file
|
|
67
|
+
--channel <channel> Send to OpenClaw channel
|
|
68
|
+
--caption <text> Message caption for channel
|
|
69
|
+
|
|
70
|
+
Formats: mp3_44100_128, mp3_44100_192, mp3_44100_64, pcm_44100, opus_48000_128
|
|
67
71
|
|
|
68
72
|
Examples:
|
|
69
73
|
node compose.js "a soft ambient piano piece about starlight"
|
|
70
|
-
node compose.js "
|
|
71
|
-
node compose.js "dreamy lo-fi beats" --instrumental --
|
|
72
|
-
node compose.js "upbeat pop" --output ./my-song.mp3
|
|
74
|
+
node compose.js "indie folk ballad" --plan --output ./song.mp3
|
|
75
|
+
node compose.js "dreamy lo-fi beats" --instrumental --duration 60
|
|
73
76
|
`);
|
|
74
77
|
}
|
|
75
78
|
|
|
76
|
-
// --- HTTP
|
|
77
|
-
function
|
|
79
|
+
// --- HTTP helpers ---
|
|
80
|
+
function apiRequest(endpoint, options = {}) {
|
|
78
81
|
return new Promise((resolve, reject) => {
|
|
79
|
-
const
|
|
80
|
-
|
|
82
|
+
const url = new URL(endpoint, API_BASE);
|
|
83
|
+
if (options.query) {
|
|
84
|
+
for (const [k, v] of Object.entries(options.query)) {
|
|
85
|
+
url.searchParams.set(k, v);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const reqOpts = {
|
|
90
|
+
method: options.method || 'POST',
|
|
91
|
+
headers: {
|
|
92
|
+
'xi-api-key': options.apiKey,
|
|
93
|
+
'Content-Type': 'application/json',
|
|
94
|
+
...options.headers,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const req = https.request(url, reqOpts, (res) => {
|
|
99
|
+
// For streaming audio, return the raw response
|
|
100
|
+
if (options.stream) {
|
|
101
|
+
resolve({ status: res.statusCode, headers: res.headers, stream: res });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
81
104
|
const chunks = [];
|
|
82
105
|
res.on('data', (c) => chunks.push(c));
|
|
83
106
|
res.on('end', () => {
|
|
@@ -90,37 +113,37 @@ function request(url, options = {}) {
|
|
|
90
113
|
});
|
|
91
114
|
});
|
|
92
115
|
req.on('error', reject);
|
|
93
|
-
if (options.body) req.write(options.body);
|
|
116
|
+
if (options.body) req.write(typeof options.body === 'string' ? options.body : JSON.stringify(options.body));
|
|
94
117
|
req.end();
|
|
95
118
|
});
|
|
96
119
|
}
|
|
97
120
|
|
|
98
|
-
function
|
|
99
|
-
return new Promise((
|
|
121
|
+
function streamToFile(stream, filePath) {
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
const file = fs.createWriteStream(filePath);
|
|
124
|
+
stream.pipe(file);
|
|
125
|
+
file.on('finish', () => { file.close(); resolve(); });
|
|
126
|
+
file.on('error', (e) => { fs.unlink(filePath, () => {}); reject(e); });
|
|
127
|
+
stream.on('error', (e) => { fs.unlink(filePath, () => {}); reject(e); });
|
|
128
|
+
});
|
|
100
129
|
}
|
|
101
130
|
|
|
102
|
-
function
|
|
131
|
+
function streamToBuffer(stream) {
|
|
103
132
|
return new Promise((resolve, reject) => {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// Follow redirect
|
|
109
|
-
downloadFile(res.headers.location, dest).then(resolve).catch(reject);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
res.pipe(file);
|
|
113
|
-
file.on('finish', () => { file.close(); resolve(); });
|
|
114
|
-
}).on('error', (e) => { fs.unlink(dest, () => {}); reject(e); });
|
|
133
|
+
const chunks = [];
|
|
134
|
+
stream.on('data', (c) => chunks.push(c));
|
|
135
|
+
stream.on('end', () => resolve(Buffer.concat(chunks)));
|
|
136
|
+
stream.on('error', reject);
|
|
115
137
|
});
|
|
116
138
|
}
|
|
117
139
|
|
|
118
140
|
// --- Main ---
|
|
119
141
|
async function main() {
|
|
120
|
-
const apiKey = process.env.
|
|
142
|
+
const apiKey = process.env.ELEVENLABS_API_KEY;
|
|
121
143
|
if (!apiKey) {
|
|
122
|
-
console.error('Error:
|
|
123
|
-
console.error('Get your API key from: https://
|
|
144
|
+
console.error('Error: ELEVENLABS_API_KEY environment variable not set');
|
|
145
|
+
console.error('Get your API key from: https://elevenlabs.io');
|
|
146
|
+
console.error('(Same key used by the voice faculty)');
|
|
124
147
|
process.exit(1);
|
|
125
148
|
}
|
|
126
149
|
|
|
@@ -130,122 +153,140 @@ async function main() {
|
|
|
130
153
|
process.exit(1);
|
|
131
154
|
}
|
|
132
155
|
|
|
133
|
-
const customMode = !!(opts.style || opts.title);
|
|
134
|
-
|
|
135
|
-
// Build payload
|
|
136
|
-
const payload = {
|
|
137
|
-
customMode,
|
|
138
|
-
instrumental: opts.instrumental,
|
|
139
|
-
model: opts.model,
|
|
140
|
-
callBackUrl: '',
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
if (customMode) {
|
|
144
|
-
payload.style = opts.style || 'pop';
|
|
145
|
-
payload.title = opts.title || 'Untitled';
|
|
146
|
-
if (!opts.instrumental) {
|
|
147
|
-
payload.prompt = opts.prompt;
|
|
148
|
-
}
|
|
149
|
-
} else {
|
|
150
|
-
payload.prompt = opts.prompt;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const modeLabel = customMode ? 'Custom' : 'Simple';
|
|
154
156
|
const typeLabel = opts.instrumental ? 'Instrumental' : 'Song';
|
|
155
|
-
|
|
157
|
+
const modeLabel = opts.plan ? 'Plan' : 'Simple';
|
|
158
|
+
console.log(`🎵 Mode: ${modeLabel} | Type: ${typeLabel}`);
|
|
156
159
|
console.log(` Prompt: ${opts.prompt.slice(0, 100)}${opts.prompt.length > 100 ? '...' : ''}`);
|
|
160
|
+
if (opts.duration) console.log(` Duration: ${opts.duration}s`);
|
|
161
|
+
|
|
162
|
+
let compositionPlan = null;
|
|
163
|
+
|
|
164
|
+
// Step 1 (optional): Generate composition plan
|
|
165
|
+
if (opts.plan) {
|
|
166
|
+
console.log('📝 Generating composition plan...');
|
|
167
|
+
const planPayload = {
|
|
168
|
+
prompt: opts.prompt,
|
|
169
|
+
model_id: 'music_v1',
|
|
170
|
+
};
|
|
171
|
+
if (opts.duration) {
|
|
172
|
+
planPayload.music_length_ms = opts.duration * 1000;
|
|
173
|
+
}
|
|
157
174
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
method: 'POST',
|
|
163
|
-
headers: {
|
|
164
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
165
|
-
'Content-Type': 'application/json',
|
|
166
|
-
},
|
|
167
|
-
body: JSON.stringify(payload),
|
|
168
|
-
});
|
|
175
|
+
const planRes = await apiRequest('/v1/music/plan', {
|
|
176
|
+
apiKey,
|
|
177
|
+
body: planPayload,
|
|
178
|
+
});
|
|
169
179
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
180
|
+
if (planRes.status !== 200) {
|
|
181
|
+
console.error(`Error: Plan API returned ${planRes.status}`);
|
|
182
|
+
console.error(typeof planRes.data === 'string' ? planRes.data : JSON.stringify(planRes.data, null, 2));
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
174
185
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
console.
|
|
178
|
-
console.
|
|
179
|
-
process.exit(1);
|
|
186
|
+
compositionPlan = planRes.data;
|
|
187
|
+
console.log(' Plan generated:');
|
|
188
|
+
console.log(` Styles: ${compositionPlan.positive_global_styles?.join(', ') || 'auto'}`);
|
|
189
|
+
console.log(` Sections: ${compositionPlan.sections?.map(s => s.section_name).join(' → ') || 'auto'}`);
|
|
180
190
|
}
|
|
181
191
|
|
|
182
|
-
|
|
183
|
-
console.log('⏳ Composing...
|
|
192
|
+
// Step 2: Stream the music
|
|
193
|
+
console.log('⏳ Composing...');
|
|
184
194
|
|
|
185
|
-
|
|
186
|
-
const pollInterval = 5000;
|
|
187
|
-
let elapsed = 0;
|
|
188
|
-
let result = null;
|
|
195
|
+
const streamPayload = { model_id: 'music_v1' };
|
|
189
196
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
{
|
|
197
|
-
method: 'GET',
|
|
198
|
-
headers: { 'Authorization': `Bearer ${apiKey}` },
|
|
199
|
-
}
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
if (statusRes.data.code !== 200) {
|
|
203
|
-
process.stdout.write(`\r Waiting... (${Math.round(elapsed / 1000)}s)`);
|
|
204
|
-
continue;
|
|
197
|
+
if (compositionPlan) {
|
|
198
|
+
streamPayload.composition_plan = compositionPlan;
|
|
199
|
+
} else {
|
|
200
|
+
streamPayload.prompt = opts.prompt;
|
|
201
|
+
if (opts.duration) {
|
|
202
|
+
streamPayload.music_length_ms = opts.duration * 1000;
|
|
205
203
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (tracks && tracks.length > 0 && tracks[0].audio_url) {
|
|
209
|
-
result = tracks[0];
|
|
210
|
-
break;
|
|
204
|
+
if (opts.instrumental) {
|
|
205
|
+
streamPayload.force_instrumental = true;
|
|
211
206
|
}
|
|
212
|
-
|
|
213
|
-
process.stdout.write(`\r Waiting... (${Math.round(elapsed / 1000)}s)`);
|
|
214
207
|
}
|
|
215
208
|
|
|
216
|
-
|
|
209
|
+
// Try /v1/music first (compose, returns full file), fallback to /v1/music/stream
|
|
210
|
+
const endpoints = ['/v1/music', '/v1/music/stream'];
|
|
211
|
+
let composeRes = null;
|
|
212
|
+
let usedEndpoint = '';
|
|
213
|
+
|
|
214
|
+
for (const ep of endpoints) {
|
|
215
|
+
composeRes = await apiRequest(ep, {
|
|
216
|
+
apiKey,
|
|
217
|
+
body: streamPayload,
|
|
218
|
+
query: { output_format: opts.format },
|
|
219
|
+
stream: true,
|
|
220
|
+
});
|
|
217
221
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
+
if (composeRes.status === 200) {
|
|
223
|
+
usedEndpoint = ep;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
222
226
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
+
// Read error for diagnostics, try next endpoint
|
|
228
|
+
const errBuf = await streamToBuffer(composeRes.stream);
|
|
229
|
+
if (ep === endpoints[endpoints.length - 1]) {
|
|
230
|
+
// Last endpoint, report error
|
|
231
|
+
const errStr = errBuf.toString();
|
|
232
|
+
console.error(`Error: Music API returned ${composeRes.status}`);
|
|
233
|
+
try {
|
|
234
|
+
console.error(JSON.stringify(JSON.parse(errStr), null, 2));
|
|
235
|
+
} catch {
|
|
236
|
+
console.error(errStr);
|
|
237
|
+
}
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
console.log(` ${ep} returned ${composeRes.status}, trying next endpoint...`);
|
|
227
241
|
}
|
|
228
242
|
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
243
|
+
// Get song_id from response headers
|
|
244
|
+
const songId = composeRes.headers['song-id'] || composeRes.headers['x-song-id'] || '';
|
|
245
|
+
|
|
246
|
+
// Determine output path
|
|
247
|
+
const ext = opts.format.startsWith('pcm') ? 'wav'
|
|
248
|
+
: opts.format.startsWith('opus') ? 'ogg'
|
|
249
|
+
: 'mp3';
|
|
250
|
+
const outPath = opts.output
|
|
251
|
+
? path.resolve(opts.output)
|
|
252
|
+
: path.resolve(`composition-${Date.now()}.${ext}`);
|
|
253
|
+
|
|
254
|
+
await streamToFile(composeRes.stream, outPath);
|
|
255
|
+
|
|
256
|
+
const stats = fs.statSync(outPath);
|
|
257
|
+
const sizeMb = (stats.size / (1024 * 1024)).toFixed(2);
|
|
258
|
+
|
|
259
|
+
console.log(`✅ Composed! Saved to: ${outPath} (${sizeMb} MB)`);
|
|
260
|
+
if (songId) console.log(` Song ID: ${songId}`);
|
|
261
|
+
|
|
262
|
+
// Send via OpenClaw (if channel provided)
|
|
263
|
+
if (opts.channel) {
|
|
264
|
+
console.log(`📤 Sending to channel: ${opts.channel}`);
|
|
265
|
+
try {
|
|
266
|
+
const { execFileSync } = require('child_process');
|
|
267
|
+
const message = opts.caption || '🎵 New composition';
|
|
268
|
+
execFileSync('openclaw', ['message', 'send', '--channel', opts.channel, '--message', message, '--media', outPath], { stdio: 'inherit' });
|
|
269
|
+
console.log(` Sent to ${opts.channel}`);
|
|
270
|
+
} catch {
|
|
271
|
+
console.log(' OpenClaw CLI not available, skipping channel send.');
|
|
272
|
+
}
|
|
235
273
|
}
|
|
236
274
|
|
|
237
275
|
// Output JSON result
|
|
238
276
|
const output = {
|
|
239
277
|
success: true,
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
duration_seconds: result.duration || 0,
|
|
278
|
+
file: outPath,
|
|
279
|
+
size_mb: parseFloat(sizeMb),
|
|
280
|
+
format: opts.format,
|
|
244
281
|
prompt: opts.prompt,
|
|
245
|
-
model: opts.model,
|
|
246
282
|
instrumental: opts.instrumental,
|
|
247
|
-
|
|
248
|
-
|
|
283
|
+
plan_mode: opts.plan,
|
|
284
|
+
duration_requested: opts.duration || 'auto',
|
|
285
|
+
song_id: songId || null,
|
|
286
|
+
plan: compositionPlan ? {
|
|
287
|
+
styles: compositionPlan.positive_global_styles || [],
|
|
288
|
+
sections: (compositionPlan.sections || []).map(s => s.section_name),
|
|
289
|
+
} : null,
|
|
249
290
|
};
|
|
250
291
|
|
|
251
292
|
console.log('\n' + JSON.stringify(output, null, 2));
|