sogni-gen 1.2.4 → 1.3.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 +63 -3
- package/SKILL.md +40 -1
- package/llm.txt +1 -0
- package/mcp-server.mjs +604 -0
- package/package.json +10 -4
- package/sogni-gen.mjs +107 -2
package/README.md
CHANGED
|
@@ -2,14 +2,45 @@
|
|
|
2
2
|
<img src="screenshot.jpg" alt="Telegram image render workflow" width="320" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
# Image & Video
|
|
5
|
+
# Sogni Gen — AI Image & Video Generation
|
|
6
6
|
|
|
7
7
|
🎨 Generate **images and videos** using [Sogni AI](https://sogni.ai)'s decentralized GPU network.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Works as an [MCP server](https://modelcontextprotocol.io/) for **Claude Code** and **Claude Desktop**, and as an [OpenClaw](https://github.com/OpenClaw/OpenClaw) plugin.
|
|
10
10
|
|
|
11
11
|
## Installation
|
|
12
12
|
|
|
13
|
+
### Claude Code (one command)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
claude mcp add sogni -- npx -y -p sogni-gen sogni-gen-mcp
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
After install, just ask Claude things like:
|
|
20
|
+
- "Generate an image of a sunset over mountains"
|
|
21
|
+
- "Make a video of a cat playing piano"
|
|
22
|
+
- "Edit this image to add a rainbow"
|
|
23
|
+
- "Check my Sogni balance"
|
|
24
|
+
|
|
25
|
+
### Claude Desktop
|
|
26
|
+
|
|
27
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"mcpServers": {
|
|
32
|
+
"sogni": {
|
|
33
|
+
"command": "npx",
|
|
34
|
+
"args": ["-y", "-p", "sogni-gen", "sogni-gen-mcp"]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Restart Claude Desktop after saving. The same natural-language commands work.
|
|
41
|
+
|
|
42
|
+
> **Note:** Both Claude Code and Claude Desktop require Sogni credentials — see [Setup](#setup) below.
|
|
43
|
+
|
|
13
44
|
### Quick Install (OpenClaw) - Recommended
|
|
14
45
|
|
|
15
46
|
Point your OpenClaw to the [`llm.txt`](https://raw.githubusercontent.com/Sogni-AI/openclaw-sogni-gen/main/llm.txt) and everything is set up — just paste the URL into Telegram, WhatsApp, or iMessages and the bot handles image and video generation automatically.
|
|
@@ -52,6 +83,7 @@ If OpenClaw loads this plugin, `sogni-gen` will read defaults from your OpenClaw
|
|
|
52
83
|
"config": {
|
|
53
84
|
"defaultImageModel": "z_image_turbo_bf16",
|
|
54
85
|
"defaultEditModel": "qwen_image_edit_2511_fp8_lightning",
|
|
86
|
+
"defaultPhotoboothModel": "coreml-sogniXLturbo_alpha1_ad",
|
|
55
87
|
"videoModels": {
|
|
56
88
|
"t2v": "wan_v2.2-14b-fp8_t2v_lightx2v",
|
|
57
89
|
"i2v": "wan_v2.2-14b-fp8_i2v_lightx2v",
|
|
@@ -123,6 +155,10 @@ node sogni-gen.mjs -m flux1-schnell-fp8 "a dragon eating tacos"
|
|
|
123
155
|
# JPG output
|
|
124
156
|
node sogni-gen.mjs --output-format jpg -o dragon.jpg "a dragon eating tacos"
|
|
125
157
|
|
|
158
|
+
# Photobooth (face transfer)
|
|
159
|
+
node sogni-gen.mjs --photobooth --ref face.jpg "80s fashion portrait"
|
|
160
|
+
node sogni-gen.mjs --photobooth --ref face.jpg -n 4 "LinkedIn professional headshot"
|
|
161
|
+
|
|
126
162
|
# Image edit with LoRA
|
|
127
163
|
node sogni-gen.mjs -c subject.jpg --lora sogni_lora_v1 --lora-strength 0.7 \
|
|
128
164
|
"add a neon cyberpunk glow"
|
|
@@ -161,6 +197,26 @@ node sogni-gen.mjs --video --estimate-video-cost --steps 20 \
|
|
|
161
197
|
-m wan_v2.2-14b-fp8_t2v_lightx2v "ocean waves at sunset"
|
|
162
198
|
```
|
|
163
199
|
|
|
200
|
+
## Photobooth (Face Transfer)
|
|
201
|
+
|
|
202
|
+
Generate stylized portraits from a face photo using InstantID ControlNet:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# Basic photobooth
|
|
206
|
+
node sogni-gen.mjs --photobooth --ref face.jpg "80s fashion portrait"
|
|
207
|
+
|
|
208
|
+
# Multiple outputs
|
|
209
|
+
node sogni-gen.mjs --photobooth --ref face.jpg -n 4 "LinkedIn professional headshot"
|
|
210
|
+
|
|
211
|
+
# Custom ControlNet tuning
|
|
212
|
+
node sogni-gen.mjs --photobooth --ref face.jpg --cn-strength 0.6 --cn-guidance-end 0.5 "oil painting"
|
|
213
|
+
|
|
214
|
+
# Custom model
|
|
215
|
+
node sogni-gen.mjs --photobooth --ref face.jpg -m coreml-dreamshaperXL_v21TurboDPMSDE "anime style"
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Uses SDXL Turbo (`coreml-sogniXLturbo_alpha1_ad`) at 1024x1024 by default. The face image is passed via `--ref` and styled according to the prompt. Cannot be combined with `--video` or `-c/--context`.
|
|
219
|
+
|
|
164
220
|
Multi-angle mode auto-builds the `<sks>` prompt and applies the `multiple_angles` LoRA.
|
|
165
221
|
`--angles-360-video` generates i2v clips between consecutive angles (including last→first) and concatenates them with ffmpeg for a seamless loop.
|
|
166
222
|
`--balance` / `--balances` does not require a prompt and exits after printing current `SPARK` and `SOGNI` balances.
|
|
@@ -221,7 +277,10 @@ Multi-angle mode auto-builds the `<sks>` prompt and applies the `multiple_angles
|
|
|
221
277
|
--auto-resize-assets Auto-resize video reference assets
|
|
222
278
|
--no-auto-resize-assets Disable auto-resize for video assets
|
|
223
279
|
--estimate-video-cost Estimate video cost and exit (requires --steps)
|
|
224
|
-
--
|
|
280
|
+
--photobooth Face transfer mode (InstantID + SDXL Turbo)
|
|
281
|
+
--cn-strength <n> ControlNet strength (default: 0.8)
|
|
282
|
+
--cn-guidance-end <n> ControlNet guidance end point (default: 0.3)
|
|
283
|
+
--ref <path|url> Reference image for i2v/s2v/animate/photobooth
|
|
225
284
|
--ref-end <path|url> End frame for i2v interpolation
|
|
226
285
|
--ref-audio <path> Reference audio for s2v
|
|
227
286
|
--ref-video <path> Reference video for animate workflows
|
|
@@ -242,6 +301,7 @@ Multi-angle mode auto-builds the `<sks>` prompt and applies the `multiple_angles
|
|
|
242
301
|
| `chroma-v.46-flash_fp8` | ~30s | Balanced |
|
|
243
302
|
| `qwen_image_edit_2511_fp8` | ~30s | Image editing with context |
|
|
244
303
|
| `qwen_image_edit_2511_fp8_lightning` | ~8s | Fast image editing |
|
|
304
|
+
| `coreml-sogniXLturbo_alpha1_ad` | Fast | Photobooth face transfer (SDXL Turbo) |
|
|
245
305
|
| `wan_v2.2-14b-fp8_t2v_lightx2v` | ~5min | Text-to-video |
|
|
246
306
|
| `wan_v2.2-14b-fp8_i2v_lightx2v` | ~3-5min | Image-to-video |
|
|
247
307
|
| `wan_v2.2-14b-fp8_s2v_lightx2v` | ~5min | Sound-to-video |
|
package/SKILL.md
CHANGED
|
@@ -110,7 +110,10 @@ node sogni-gen.mjs -q -o /tmp/cat.png "a cat wearing a hat"
|
|
|
110
110
|
| `--auto-resize-assets` | Auto-resize video assets | true |
|
|
111
111
|
| `--no-auto-resize-assets` | Disable auto-resize | - |
|
|
112
112
|
| `--estimate-video-cost` | Estimate video cost and exit (requires --steps) | - |
|
|
113
|
-
| `--
|
|
113
|
+
| `--photobooth` | Face transfer mode (InstantID + SDXL Turbo) | - |
|
|
114
|
+
| `--cn-strength <n>` | ControlNet strength (photobooth) | 0.8 |
|
|
115
|
+
| `--cn-guidance-end <n>` | ControlNet guidance end point (photobooth) | 0.3 |
|
|
116
|
+
| `--ref <path>` | Reference image for video or photobooth face | required for video/photobooth |
|
|
114
117
|
| `--ref-end <path>` | End frame for i2v interpolation | - |
|
|
115
118
|
| `--ref-audio <path>` | Reference audio for s2v | - |
|
|
116
119
|
| `--ref-video <path>` | Reference video for animate workflows | - |
|
|
@@ -134,6 +137,7 @@ When installed as an OpenClaw plugin, `sogni-gen` will read defaults from:
|
|
|
134
137
|
"config": {
|
|
135
138
|
"defaultImageModel": "z_image_turbo_bf16",
|
|
136
139
|
"defaultEditModel": "qwen_image_edit_2511_fp8_lightning",
|
|
140
|
+
"defaultPhotoboothModel": "coreml-sogniXLturbo_alpha1_ad",
|
|
137
141
|
"videoModels": {
|
|
138
142
|
"t2v": "wan_v2.2-14b-fp8_t2v_lightx2v",
|
|
139
143
|
"i2v": "wan_v2.2-14b-fp8_i2v_lightx2v",
|
|
@@ -177,6 +181,7 @@ Seed strategies: `prompt-hash` (deterministic) or `random`.
|
|
|
177
181
|
| `chroma-v.46-flash_fp8` | Medium | Balanced |
|
|
178
182
|
| `qwen_image_edit_2511_fp8` | Medium | Image editing with context (up to 3) |
|
|
179
183
|
| `qwen_image_edit_2511_fp8_lightning` | Fast | Quick image editing |
|
|
184
|
+
| `coreml-sogniXLturbo_alpha1_ad` | Fast | Photobooth face transfer (SDXL Turbo) |
|
|
180
185
|
|
|
181
186
|
## Video Models
|
|
182
187
|
|
|
@@ -206,6 +211,32 @@ node sogni-gen.mjs --last-image "make it more vibrant"
|
|
|
206
211
|
|
|
207
212
|
When context images are provided without `-m`, defaults to `qwen_image_edit_2511_fp8_lightning`.
|
|
208
213
|
|
|
214
|
+
## Photobooth (Face Transfer)
|
|
215
|
+
|
|
216
|
+
Generate stylized portraits from a face photo using InstantID ControlNet. When a user mentions "photobooth", wants a stylized portrait of themselves, or asks to transfer their face into a style, use `--photobooth` with `--ref` pointing to their face image.
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
# Basic photobooth
|
|
220
|
+
node sogni-gen.mjs --photobooth --ref face.jpg "80s fashion portrait"
|
|
221
|
+
|
|
222
|
+
# Multiple outputs
|
|
223
|
+
node sogni-gen.mjs --photobooth --ref face.jpg -n 4 "LinkedIn professional headshot"
|
|
224
|
+
|
|
225
|
+
# Custom ControlNet tuning
|
|
226
|
+
node sogni-gen.mjs --photobooth --ref face.jpg --cn-strength 0.6 --cn-guidance-end 0.5 "oil painting"
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Uses SDXL Turbo (`coreml-sogniXLturbo_alpha1_ad`) at 1024x1024 by default. The face image is passed via `--ref` and styled according to the prompt. Cannot be combined with `--video` or `-c/--context`.
|
|
230
|
+
|
|
231
|
+
**Agent usage:**
|
|
232
|
+
```bash
|
|
233
|
+
# Photobooth: stylize a face photo
|
|
234
|
+
node {{skillDir}}/sogni-gen.mjs -q --photobooth --ref /path/to/face.jpg -o /tmp/stylized.png "80s fashion portrait"
|
|
235
|
+
|
|
236
|
+
# Multiple photobooth outputs
|
|
237
|
+
node {{skillDir}}/sogni-gen.mjs -q --photobooth --ref /path/to/face.jpg -n 4 -o /tmp/stylized.png "LinkedIn professional headshot"
|
|
238
|
+
```
|
|
239
|
+
|
|
209
240
|
## Multiple Angles (Turnaround)
|
|
210
241
|
|
|
211
242
|
Generate specific camera angles from a single reference image using the Multiple Angles LoRA:
|
|
@@ -319,6 +350,11 @@ ls -la ~/.clawdbot/media/inbound/*.jpg | tail -3
|
|
|
319
350
|
cp ~/.clawdbot/media/inbound/<latest>.jpg /tmp/to_restore.jpg
|
|
320
351
|
```
|
|
321
352
|
|
|
353
|
+
## IMPORTANT KEYWORD RULE
|
|
354
|
+
|
|
355
|
+
- If the user message includes the word "photobooth" (case-insensitive), always use `--photobooth` mode with `--ref` set to the user-provided face image.
|
|
356
|
+
- Prioritize this rule over generic image-edit flows (`-c`) for that request.
|
|
357
|
+
|
|
322
358
|
## Agent Usage
|
|
323
359
|
|
|
324
360
|
When user asks to generate/draw/create an image:
|
|
@@ -336,6 +372,9 @@ node {{skillDir}}/sogni-gen.mjs -q --video --ref /path/to/image.png -o /tmp/vide
|
|
|
336
372
|
# Generate text-to-video
|
|
337
373
|
node {{skillDir}}/sogni-gen.mjs -q --video -o /tmp/video.mp4 "ocean waves at sunset"
|
|
338
374
|
|
|
375
|
+
# Photobooth: stylize a face photo
|
|
376
|
+
node {{skillDir}}/sogni-gen.mjs -q --photobooth --ref /path/to/face.jpg -o /tmp/stylized.png "80s fashion portrait"
|
|
377
|
+
|
|
339
378
|
# Check current SPARK/SOGNI balances (no prompt required)
|
|
340
379
|
node {{skillDir}}/sogni-gen.mjs --json --balance
|
|
341
380
|
|
package/llm.txt
CHANGED
|
@@ -138,6 +138,7 @@ node {{skillDir}}/sogni-gen.mjs --json --balance
|
|
|
138
138
|
|
|
139
139
|
## Agent Behavior Guidelines
|
|
140
140
|
|
|
141
|
+
0. If the user includes the keyword "photobooth" (case-insensitive), always use `--photobooth` with `--ref` to the user face image. Do not fall back to `-c` edit flow for that request.
|
|
141
142
|
1. When the user asks to "draw", "generate", "create", or "make" an image: generate an image and send it.
|
|
142
143
|
2. When they ask to "animate", "make a video", or "create a video": use --video mode.
|
|
143
144
|
3. When they send a photo and ask to edit/change/modify it: use -c with their image.
|
package/mcp-server.mjs
ADDED
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* sogni-gen MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Exposes Sogni AI image/video generation as MCP tools for Claude Code
|
|
6
|
+
* and Claude Desktop. Wraps the sogni-gen CLI using its --json mode.
|
|
7
|
+
*
|
|
8
|
+
* Install (Claude Code):
|
|
9
|
+
* claude mcp add sogni -- npx -y -p sogni-gen sogni-gen-mcp
|
|
10
|
+
*
|
|
11
|
+
* Install (Claude Desktop – add to claude_desktop_config.json):
|
|
12
|
+
* { "mcpServers": { "sogni": { "command": "npx", "args": ["-y", "-p", "sogni-gen", "sogni-gen-mcp"] } } }
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
16
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
17
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
18
|
+
import { spawn } from 'child_process';
|
|
19
|
+
import { fileURLToPath } from 'url';
|
|
20
|
+
import { dirname, join } from 'path';
|
|
21
|
+
import { existsSync } from 'fs';
|
|
22
|
+
import { homedir } from 'os';
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Paths
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
29
|
+
const __dirname = dirname(__filename);
|
|
30
|
+
const SOGNI_GEN = join(__dirname, 'sogni-gen.mjs');
|
|
31
|
+
const CREDENTIALS_PATH = join(homedir(), '.config', 'sogni', 'credentials');
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// CLI spawning helper
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Spawn `node sogni-gen.mjs --json ...args`, collect stdout, parse JSON.
|
|
39
|
+
* Returns the parsed object on success or throws on failure.
|
|
40
|
+
*/
|
|
41
|
+
function runSogniGen(args, { timeoutMs = 30_000 } = {}) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const child = spawn(process.execPath, [SOGNI_GEN, '--json', '--quiet', ...args], {
|
|
44
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
45
|
+
timeout: timeoutMs,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const stdoutChunks = [];
|
|
49
|
+
const stderrChunks = [];
|
|
50
|
+
|
|
51
|
+
child.stdout.on('data', (chunk) => stdoutChunks.push(chunk));
|
|
52
|
+
child.stderr.on('data', (chunk) => stderrChunks.push(chunk));
|
|
53
|
+
|
|
54
|
+
child.on('error', (err) => {
|
|
55
|
+
reject(new Error(`Failed to spawn sogni-gen: ${err.message}`));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
child.on('close', (code) => {
|
|
59
|
+
const stdout = Buffer.concat(stdoutChunks).toString('utf8').trim();
|
|
60
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf8').trim();
|
|
61
|
+
|
|
62
|
+
if (!stdout) {
|
|
63
|
+
reject(new Error(stderr || `sogni-gen exited with code ${code} and no output`));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const result = JSON.parse(stdout);
|
|
69
|
+
resolve(result);
|
|
70
|
+
} catch {
|
|
71
|
+
reject(new Error(`Failed to parse sogni-gen output: ${stdout.slice(0, 500)}`));
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Credential check helper
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
function checkCredentials() {
|
|
82
|
+
if (existsSync(CREDENTIALS_PATH)) return null;
|
|
83
|
+
if (process.env.SOGNI_USERNAME && process.env.SOGNI_PASSWORD) return null;
|
|
84
|
+
return {
|
|
85
|
+
content: [
|
|
86
|
+
{
|
|
87
|
+
type: 'text',
|
|
88
|
+
text: [
|
|
89
|
+
'Sogni credentials not found. Please set up credentials:',
|
|
90
|
+
'',
|
|
91
|
+
'1. Create a Sogni account at https://sogni.ai',
|
|
92
|
+
'2. Create the credentials file:',
|
|
93
|
+
'',
|
|
94
|
+
' mkdir -p ~/.config/sogni',
|
|
95
|
+
' cat > ~/.config/sogni/credentials << \'EOF\'',
|
|
96
|
+
' SOGNI_USERNAME=your_username',
|
|
97
|
+
' SOGNI_PASSWORD=your_password',
|
|
98
|
+
' EOF',
|
|
99
|
+
' chmod 600 ~/.config/sogni/credentials',
|
|
100
|
+
'',
|
|
101
|
+
'Or set SOGNI_USERNAME and SOGNI_PASSWORD environment variables.',
|
|
102
|
+
].join('\n'),
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
isError: true,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Result formatting
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
function formatSuccess(result) {
|
|
114
|
+
const parts = [];
|
|
115
|
+
|
|
116
|
+
if (result.type === 'balance') {
|
|
117
|
+
parts.push(`SPARK: ${result.spark ?? 'N/A'}`);
|
|
118
|
+
parts.push(`SOGNI: ${result.sogni ?? 'N/A'}`);
|
|
119
|
+
return { content: [{ type: 'text', text: parts.join('\n') }] };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Image / video result
|
|
123
|
+
if (result.prompt) parts.push(`Prompt: ${result.prompt}`);
|
|
124
|
+
parts.push(`Model: ${result.model}`);
|
|
125
|
+
parts.push(`Size: ${result.width}x${result.height}`);
|
|
126
|
+
if (result.seed != null) parts.push(`Seed: ${result.seed}`);
|
|
127
|
+
|
|
128
|
+
if (result.type === 'video') {
|
|
129
|
+
if (result.workflow) parts.push(`Workflow: ${result.workflow}`);
|
|
130
|
+
if (result.duration) parts.push(`Duration: ${result.duration}s`);
|
|
131
|
+
if (result.fps) parts.push(`FPS: ${result.fps}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (result.localPath) parts.push(`Saved to: ${result.localPath}`);
|
|
135
|
+
|
|
136
|
+
// URLs
|
|
137
|
+
const urls = result.urls || [];
|
|
138
|
+
if (urls.length > 0) {
|
|
139
|
+
parts.push('');
|
|
140
|
+
urls.forEach((url, i) => {
|
|
141
|
+
parts.push(urls.length === 1 ? `URL: ${url}` : `URL #${i + 1}: ${url}`);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const content = [{ type: 'text', text: parts.join('\n') }];
|
|
146
|
+
|
|
147
|
+
// Also include image URLs as image content so Claude can see them
|
|
148
|
+
for (const url of urls) {
|
|
149
|
+
if (/\.(png|jpg|jpeg|webp|gif)(\?|$)/i.test(url)) {
|
|
150
|
+
content.push({ type: 'image', data: url, mimeType: 'image/png' });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { content };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function formatError(result) {
|
|
158
|
+
const parts = [`Error: ${result.error}`];
|
|
159
|
+
if (result.errorCode) parts.push(`Code: ${result.errorCode}`);
|
|
160
|
+
if (result.hint) parts.push(`Hint: ${result.hint}`);
|
|
161
|
+
return { content: [{ type: 'text', text: parts.join('\n') }], isError: true };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function formatResult(result) {
|
|
165
|
+
if (result.success === false) return formatError(result);
|
|
166
|
+
return formatSuccess(result);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Tool definitions
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
const IMAGE_MODEL_TABLE = `Image Models:
|
|
174
|
+
z_image_turbo_bf16 — Fast (~5-10s), general purpose (default)
|
|
175
|
+
flux1-schnell-fp8 — Very fast (~3-5s), quick iterations
|
|
176
|
+
flux2_dev_fp8 — Slow (~2min), high quality
|
|
177
|
+
chroma-v.46-flash_fp8 — Medium (~30s), balanced
|
|
178
|
+
qwen_image_edit_2511_fp8 — Medium (~30s), image editing with context
|
|
179
|
+
qwen_image_edit_2511_fp8_lightning — Fast (~8s), quick image editing`;
|
|
180
|
+
|
|
181
|
+
const VIDEO_MODEL_TABLE = `Video Models (auto-selected per workflow):
|
|
182
|
+
wan_v2.2-14b-fp8_t2v_lightx2v — Text-to-video (~5min)
|
|
183
|
+
wan_v2.2-14b-fp8_i2v_lightx2v — Image-to-video (~3-5min)
|
|
184
|
+
wan_v2.2-14b-fp8_s2v_lightx2v — Sound-to-video (~5min)
|
|
185
|
+
wan_v2.2-14b-fp8_animate-move_lightx2v — Animate-move (~5min)
|
|
186
|
+
wan_v2.2-14b-fp8_animate-replace_lightx2v — Animate-replace (~5min)`;
|
|
187
|
+
|
|
188
|
+
const TOOLS = [
|
|
189
|
+
{
|
|
190
|
+
name: 'generate_image',
|
|
191
|
+
description: `Generate an image using Sogni AI's decentralized GPU network.
|
|
192
|
+
|
|
193
|
+
${IMAGE_MODEL_TABLE}
|
|
194
|
+
|
|
195
|
+
Cost: Uses Spark tokens. 512x512 is most cost-efficient. Claim 50 free daily Spark at https://app.sogni.ai/`,
|
|
196
|
+
inputSchema: {
|
|
197
|
+
type: 'object',
|
|
198
|
+
properties: {
|
|
199
|
+
prompt: {
|
|
200
|
+
type: 'string',
|
|
201
|
+
description: 'Image description / generation prompt',
|
|
202
|
+
},
|
|
203
|
+
model: {
|
|
204
|
+
type: 'string',
|
|
205
|
+
description: 'Model ID (default: z_image_turbo_bf16)',
|
|
206
|
+
},
|
|
207
|
+
width: {
|
|
208
|
+
type: 'number',
|
|
209
|
+
description: 'Image width in pixels (default: 512)',
|
|
210
|
+
},
|
|
211
|
+
height: {
|
|
212
|
+
type: 'number',
|
|
213
|
+
description: 'Image height in pixels (default: 512)',
|
|
214
|
+
},
|
|
215
|
+
count: {
|
|
216
|
+
type: 'number',
|
|
217
|
+
description: 'Number of images to generate (default: 1)',
|
|
218
|
+
},
|
|
219
|
+
seed: {
|
|
220
|
+
type: 'number',
|
|
221
|
+
description: 'Specific seed for reproducibility',
|
|
222
|
+
},
|
|
223
|
+
output: {
|
|
224
|
+
type: 'string',
|
|
225
|
+
description: 'Save image to this file path',
|
|
226
|
+
},
|
|
227
|
+
output_format: {
|
|
228
|
+
type: 'string',
|
|
229
|
+
enum: ['png', 'jpg'],
|
|
230
|
+
description: 'Output format (default: png)',
|
|
231
|
+
},
|
|
232
|
+
loras: {
|
|
233
|
+
type: 'array',
|
|
234
|
+
items: { type: 'string' },
|
|
235
|
+
description: 'LoRA model IDs',
|
|
236
|
+
},
|
|
237
|
+
lora_strengths: {
|
|
238
|
+
type: 'array',
|
|
239
|
+
items: { type: 'number' },
|
|
240
|
+
description: 'LoRA strengths (parallel to loras array)',
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
required: ['prompt'],
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
name: 'generate_video',
|
|
248
|
+
description: `Generate a video using Sogni AI's decentralized GPU network.
|
|
249
|
+
|
|
250
|
+
Workflows:
|
|
251
|
+
t2v — Text-to-video (default). Just provide a prompt.
|
|
252
|
+
i2v — Image-to-video. Provide ref (reference image). Supports looping.
|
|
253
|
+
s2v — Sound-to-video. Provide ref (face image) + ref_audio.
|
|
254
|
+
animate-move — Transfer motion from ref_video to ref image.
|
|
255
|
+
animate-replace — Replace subject in ref_video with ref image.
|
|
256
|
+
|
|
257
|
+
${VIDEO_MODEL_TABLE}
|
|
258
|
+
|
|
259
|
+
Video dimensions must be divisible by 16, min 480px, max 1536px. Generation takes 3-5 minutes.
|
|
260
|
+
Cost: Uses Spark tokens. Claim 50 free daily Spark at https://app.sogni.ai/`,
|
|
261
|
+
inputSchema: {
|
|
262
|
+
type: 'object',
|
|
263
|
+
properties: {
|
|
264
|
+
prompt: {
|
|
265
|
+
type: 'string',
|
|
266
|
+
description: 'Video description / generation prompt',
|
|
267
|
+
},
|
|
268
|
+
workflow: {
|
|
269
|
+
type: 'string',
|
|
270
|
+
enum: ['t2v', 'i2v', 's2v', 'animate-move', 'animate-replace'],
|
|
271
|
+
description: 'Video workflow (default: t2v, auto-inferred from provided refs)',
|
|
272
|
+
},
|
|
273
|
+
model: {
|
|
274
|
+
type: 'string',
|
|
275
|
+
description: 'Model ID (auto-selected per workflow by default)',
|
|
276
|
+
},
|
|
277
|
+
width: {
|
|
278
|
+
type: 'number',
|
|
279
|
+
description: 'Video width in pixels (default: 512, must be divisible by 16)',
|
|
280
|
+
},
|
|
281
|
+
height: {
|
|
282
|
+
type: 'number',
|
|
283
|
+
description: 'Video height in pixels (default: 512, must be divisible by 16)',
|
|
284
|
+
},
|
|
285
|
+
fps: {
|
|
286
|
+
type: 'number',
|
|
287
|
+
description: 'Frames per second (default: 16)',
|
|
288
|
+
},
|
|
289
|
+
duration: {
|
|
290
|
+
type: 'number',
|
|
291
|
+
description: 'Duration in seconds (default: 5)',
|
|
292
|
+
},
|
|
293
|
+
frames: {
|
|
294
|
+
type: 'number',
|
|
295
|
+
description: 'Override total frame count (alternative to duration)',
|
|
296
|
+
},
|
|
297
|
+
ref: {
|
|
298
|
+
type: 'string',
|
|
299
|
+
description: 'Reference image path or URL (for i2v, s2v, animate workflows)',
|
|
300
|
+
},
|
|
301
|
+
ref_end: {
|
|
302
|
+
type: 'string',
|
|
303
|
+
description: 'End frame image path or URL (for i2v interpolation)',
|
|
304
|
+
},
|
|
305
|
+
ref_audio: {
|
|
306
|
+
type: 'string',
|
|
307
|
+
description: 'Reference audio file path (for s2v workflow)',
|
|
308
|
+
},
|
|
309
|
+
ref_video: {
|
|
310
|
+
type: 'string',
|
|
311
|
+
description: 'Reference video file path (for animate workflows)',
|
|
312
|
+
},
|
|
313
|
+
seed: {
|
|
314
|
+
type: 'number',
|
|
315
|
+
description: 'Specific seed for reproducibility',
|
|
316
|
+
},
|
|
317
|
+
output: {
|
|
318
|
+
type: 'string',
|
|
319
|
+
description: 'Save video to this file path',
|
|
320
|
+
},
|
|
321
|
+
looping: {
|
|
322
|
+
type: 'boolean',
|
|
323
|
+
description: 'Generate seamless loop (i2v only)',
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
required: ['prompt'],
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
name: 'edit_image',
|
|
331
|
+
description: `Edit or transform an existing image using Sogni AI (Qwen image editing models).
|
|
332
|
+
|
|
333
|
+
Provide 1-3 context images and a prompt describing the desired edit. Examples:
|
|
334
|
+
- "make the background a beach"
|
|
335
|
+
- "apply pop art style"
|
|
336
|
+
- "remove the person on the left"
|
|
337
|
+
- "add a rainbow in the sky"
|
|
338
|
+
|
|
339
|
+
Models:
|
|
340
|
+
qwen_image_edit_2511_fp8_lightning — Fast (~8s), default
|
|
341
|
+
qwen_image_edit_2511_fp8 — Medium (~30s), higher quality`,
|
|
342
|
+
inputSchema: {
|
|
343
|
+
type: 'object',
|
|
344
|
+
properties: {
|
|
345
|
+
prompt: {
|
|
346
|
+
type: 'string',
|
|
347
|
+
description: 'Editing instruction describing the desired change',
|
|
348
|
+
},
|
|
349
|
+
context_images: {
|
|
350
|
+
type: 'array',
|
|
351
|
+
items: { type: 'string' },
|
|
352
|
+
description: 'Image file paths or URLs to edit (1-3 images)',
|
|
353
|
+
minItems: 1,
|
|
354
|
+
maxItems: 3,
|
|
355
|
+
},
|
|
356
|
+
model: {
|
|
357
|
+
type: 'string',
|
|
358
|
+
description: 'Model ID (default: qwen_image_edit_2511_fp8_lightning)',
|
|
359
|
+
},
|
|
360
|
+
width: {
|
|
361
|
+
type: 'number',
|
|
362
|
+
description: 'Output width in pixels',
|
|
363
|
+
},
|
|
364
|
+
height: {
|
|
365
|
+
type: 'number',
|
|
366
|
+
description: 'Output height in pixels',
|
|
367
|
+
},
|
|
368
|
+
output: {
|
|
369
|
+
type: 'string',
|
|
370
|
+
description: 'Save edited image to this file path',
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
required: ['prompt', 'context_images'],
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
name: 'photobooth',
|
|
378
|
+
description: `Generate stylized portraits from a face photo using InstantID face transfer.
|
|
379
|
+
|
|
380
|
+
Provide a face reference image and a style prompt. Examples:
|
|
381
|
+
- "80s fashion portrait"
|
|
382
|
+
- "LinkedIn professional headshot"
|
|
383
|
+
- "oil painting Renaissance style"
|
|
384
|
+
- "anime character"
|
|
385
|
+
|
|
386
|
+
Uses SDXL Turbo (coreml-sogniXLturbo_alpha1_ad) at 1024x1024 by default.
|
|
387
|
+
The face likeness is preserved while applying the style from the prompt.`,
|
|
388
|
+
inputSchema: {
|
|
389
|
+
type: 'object',
|
|
390
|
+
properties: {
|
|
391
|
+
prompt: {
|
|
392
|
+
type: 'string',
|
|
393
|
+
description: 'Style/scene description for the portrait',
|
|
394
|
+
},
|
|
395
|
+
reference_face: {
|
|
396
|
+
type: 'string',
|
|
397
|
+
description: 'Face image file path or URL',
|
|
398
|
+
},
|
|
399
|
+
model: {
|
|
400
|
+
type: 'string',
|
|
401
|
+
description: 'Model ID (default: coreml-sogniXLturbo_alpha1_ad)',
|
|
402
|
+
},
|
|
403
|
+
cn_strength: {
|
|
404
|
+
type: 'number',
|
|
405
|
+
description: 'ControlNet strength — higher = more face likeness (default: 0.8)',
|
|
406
|
+
},
|
|
407
|
+
cn_guidance_end: {
|
|
408
|
+
type: 'number',
|
|
409
|
+
description: 'ControlNet guidance end point (default: 0.3)',
|
|
410
|
+
},
|
|
411
|
+
width: {
|
|
412
|
+
type: 'number',
|
|
413
|
+
description: 'Output width in pixels (default: 1024)',
|
|
414
|
+
},
|
|
415
|
+
height: {
|
|
416
|
+
type: 'number',
|
|
417
|
+
description: 'Output height in pixels (default: 1024)',
|
|
418
|
+
},
|
|
419
|
+
count: {
|
|
420
|
+
type: 'number',
|
|
421
|
+
description: 'Number of images to generate (default: 1)',
|
|
422
|
+
},
|
|
423
|
+
output: {
|
|
424
|
+
type: 'string',
|
|
425
|
+
description: 'Save image to this file path',
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
required: ['prompt', 'reference_face'],
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
name: 'check_balance',
|
|
433
|
+
description:
|
|
434
|
+
'Check your current Sogni token balances (SPARK and SOGNI). Free daily Spark tokens can be claimed at https://app.sogni.ai/',
|
|
435
|
+
inputSchema: {
|
|
436
|
+
type: 'object',
|
|
437
|
+
properties: {},
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
name: 'list_models',
|
|
442
|
+
description:
|
|
443
|
+
'List all available Sogni AI models for image generation, image editing, photobooth, and video generation with speed estimates.',
|
|
444
|
+
inputSchema: {
|
|
445
|
+
type: 'object',
|
|
446
|
+
properties: {},
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
];
|
|
450
|
+
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
452
|
+
// Tool handlers
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
|
|
455
|
+
async function handleGenerateImage(params) {
|
|
456
|
+
const credErr = checkCredentials();
|
|
457
|
+
if (credErr) return credErr;
|
|
458
|
+
|
|
459
|
+
const args = [params.prompt];
|
|
460
|
+
if (params.model) args.push('-m', params.model);
|
|
461
|
+
if (params.width) args.push('-w', String(params.width));
|
|
462
|
+
if (params.height) args.push('-h', String(params.height));
|
|
463
|
+
if (params.count) args.push('-n', String(params.count));
|
|
464
|
+
if (params.seed != null) args.push('-s', String(params.seed));
|
|
465
|
+
if (params.output) args.push('-o', params.output);
|
|
466
|
+
if (params.output_format) args.push('--output-format', params.output_format);
|
|
467
|
+
if (params.loras?.length) args.push('--loras', params.loras.join(','));
|
|
468
|
+
if (params.lora_strengths?.length) args.push('--lora-strengths', params.lora_strengths.join(','));
|
|
469
|
+
|
|
470
|
+
const result = await runSogniGen(args, { timeoutMs: 60_000 });
|
|
471
|
+
return formatResult(result);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function handleGenerateVideo(params) {
|
|
475
|
+
const credErr = checkCredentials();
|
|
476
|
+
if (credErr) return credErr;
|
|
477
|
+
|
|
478
|
+
const args = ['--video', params.prompt];
|
|
479
|
+
if (params.workflow) args.push('--workflow', params.workflow);
|
|
480
|
+
if (params.model) args.push('-m', params.model);
|
|
481
|
+
if (params.width) args.push('-w', String(params.width));
|
|
482
|
+
if (params.height) args.push('-h', String(params.height));
|
|
483
|
+
if (params.fps) args.push('--fps', String(params.fps));
|
|
484
|
+
if (params.duration) args.push('--duration', String(params.duration));
|
|
485
|
+
if (params.frames) args.push('--frames', String(params.frames));
|
|
486
|
+
if (params.ref) args.push('--ref', params.ref);
|
|
487
|
+
if (params.ref_end) args.push('--ref-end', params.ref_end);
|
|
488
|
+
if (params.ref_audio) args.push('--ref-audio', params.ref_audio);
|
|
489
|
+
if (params.ref_video) args.push('--ref-video', params.ref_video);
|
|
490
|
+
if (params.seed != null) args.push('-s', String(params.seed));
|
|
491
|
+
if (params.output) args.push('-o', params.output);
|
|
492
|
+
if (params.looping) args.push('--looping');
|
|
493
|
+
|
|
494
|
+
const result = await runSogniGen(args, { timeoutMs: 600_000 });
|
|
495
|
+
return formatResult(result);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function handleEditImage(params) {
|
|
499
|
+
const credErr = checkCredentials();
|
|
500
|
+
if (credErr) return credErr;
|
|
501
|
+
|
|
502
|
+
const args = [];
|
|
503
|
+
for (const img of params.context_images) {
|
|
504
|
+
args.push('-c', img);
|
|
505
|
+
}
|
|
506
|
+
args.push(params.prompt);
|
|
507
|
+
if (params.model) args.push('-m', params.model);
|
|
508
|
+
if (params.width) args.push('-w', String(params.width));
|
|
509
|
+
if (params.height) args.push('-h', String(params.height));
|
|
510
|
+
if (params.output) args.push('-o', params.output);
|
|
511
|
+
|
|
512
|
+
const result = await runSogniGen(args, { timeoutMs: 60_000 });
|
|
513
|
+
return formatResult(result);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function handlePhotobooth(params) {
|
|
517
|
+
const credErr = checkCredentials();
|
|
518
|
+
if (credErr) return credErr;
|
|
519
|
+
|
|
520
|
+
const args = ['--photobooth', '--ref', params.reference_face, params.prompt];
|
|
521
|
+
if (params.model) args.push('-m', params.model);
|
|
522
|
+
if (params.cn_strength != null) args.push('--cn-strength', String(params.cn_strength));
|
|
523
|
+
if (params.cn_guidance_end != null) args.push('--cn-guidance-end', String(params.cn_guidance_end));
|
|
524
|
+
if (params.width) args.push('-w', String(params.width));
|
|
525
|
+
if (params.height) args.push('-h', String(params.height));
|
|
526
|
+
if (params.count) args.push('-n', String(params.count));
|
|
527
|
+
if (params.output) args.push('-o', params.output);
|
|
528
|
+
|
|
529
|
+
const result = await runSogniGen(args, { timeoutMs: 60_000 });
|
|
530
|
+
return formatResult(result);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async function handleCheckBalance() {
|
|
534
|
+
const credErr = checkCredentials();
|
|
535
|
+
if (credErr) return credErr;
|
|
536
|
+
|
|
537
|
+
const result = await runSogniGen(['--balance'], { timeoutMs: 30_000 });
|
|
538
|
+
return formatResult(result);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function handleListModels() {
|
|
542
|
+
const text = `${IMAGE_MODEL_TABLE}
|
|
543
|
+
|
|
544
|
+
Photobooth Model:
|
|
545
|
+
coreml-sogniXLturbo_alpha1_ad — Fast, face transfer (SDXL Turbo, default for --photobooth)
|
|
546
|
+
|
|
547
|
+
${VIDEO_MODEL_TABLE}
|
|
548
|
+
|
|
549
|
+
Defaults:
|
|
550
|
+
Image generation: z_image_turbo_bf16
|
|
551
|
+
Image editing: qwen_image_edit_2511_fp8_lightning
|
|
552
|
+
Photobooth: coreml-sogniXLturbo_alpha1_ad
|
|
553
|
+
Video: auto-selected per workflow (t2v/i2v/s2v/animate-move/animate-replace)`;
|
|
554
|
+
|
|
555
|
+
return { content: [{ type: 'text', text }] };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// ---------------------------------------------------------------------------
|
|
559
|
+
// Server setup
|
|
560
|
+
// ---------------------------------------------------------------------------
|
|
561
|
+
|
|
562
|
+
const server = new Server(
|
|
563
|
+
{ name: 'sogni', version: '1.3.0' },
|
|
564
|
+
{ capabilities: { tools: {} } },
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
568
|
+
|
|
569
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
570
|
+
const { name, arguments: params } = request.params;
|
|
571
|
+
try {
|
|
572
|
+
switch (name) {
|
|
573
|
+
case 'generate_image':
|
|
574
|
+
return await handleGenerateImage(params);
|
|
575
|
+
case 'generate_video':
|
|
576
|
+
return await handleGenerateVideo(params);
|
|
577
|
+
case 'edit_image':
|
|
578
|
+
return await handleEditImage(params);
|
|
579
|
+
case 'photobooth':
|
|
580
|
+
return await handlePhotobooth(params);
|
|
581
|
+
case 'check_balance':
|
|
582
|
+
return await handleCheckBalance();
|
|
583
|
+
case 'list_models':
|
|
584
|
+
return handleListModels();
|
|
585
|
+
default:
|
|
586
|
+
return {
|
|
587
|
+
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
588
|
+
isError: true,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
} catch (err) {
|
|
592
|
+
return {
|
|
593
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
594
|
+
isError: true,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// ---------------------------------------------------------------------------
|
|
600
|
+
// Start
|
|
601
|
+
// ---------------------------------------------------------------------------
|
|
602
|
+
|
|
603
|
+
const transport = new StdioServerTransport();
|
|
604
|
+
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sogni-gen",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Sogni AI image generation plugin for
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Sogni AI image & video generation — OpenClaw plugin and MCP server for Claude Code / Claude Desktop",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "sogni-gen.mjs",
|
|
7
7
|
"bin": {
|
|
8
|
-
"sogni-gen": "sogni-gen.mjs"
|
|
8
|
+
"sogni-gen": "sogni-gen.mjs",
|
|
9
|
+
"sogni-gen-mcp": "mcp-server.mjs"
|
|
9
10
|
},
|
|
10
11
|
"scripts": {
|
|
11
12
|
"test": "node --test test/*.test.mjs test/*.integration.mjs",
|
|
@@ -16,7 +17,11 @@
|
|
|
16
17
|
"ai",
|
|
17
18
|
"image-generation",
|
|
18
19
|
"openclaw",
|
|
19
|
-
"plugin"
|
|
20
|
+
"plugin",
|
|
21
|
+
"mcp",
|
|
22
|
+
"claude",
|
|
23
|
+
"claude-code",
|
|
24
|
+
"claude-desktop"
|
|
20
25
|
],
|
|
21
26
|
"author": "Mauvis Ledford",
|
|
22
27
|
"license": "MIT",
|
|
@@ -24,6 +29,7 @@
|
|
|
24
29
|
"node": ">=22.22.0"
|
|
25
30
|
},
|
|
26
31
|
"dependencies": {
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
27
33
|
"@sogni-ai/sogni-client-wrapper": "^1.4.2",
|
|
28
34
|
"json5": "^2.2.3"
|
|
29
35
|
},
|
package/sogni-gen.mjs
CHANGED
|
@@ -593,7 +593,10 @@ const options = {
|
|
|
593
593
|
refAudio: null, // Reference audio for s2v
|
|
594
594
|
refVideo: null, // Reference video for animate workflows
|
|
595
595
|
contextImages: [], // Context images for image editing
|
|
596
|
-
looping: false // Create looping video (i2v only): generate A→B then B→A and concatenate
|
|
596
|
+
looping: false, // Create looping video (i2v only): generate A→B then B→A and concatenate
|
|
597
|
+
photobooth: false, // Photobooth mode (InstantID face transfer)
|
|
598
|
+
cnStrength: null, // ControlNet strength override
|
|
599
|
+
cnGuidanceEnd: null // ControlNet guidance end override
|
|
597
600
|
};
|
|
598
601
|
const cliSet = {
|
|
599
602
|
output: false,
|
|
@@ -630,7 +633,10 @@ const cliSet = {
|
|
|
630
633
|
refImageEnd: false,
|
|
631
634
|
refAudio: false,
|
|
632
635
|
refVideo: false,
|
|
633
|
-
context: false
|
|
636
|
+
context: false,
|
|
637
|
+
photobooth: false,
|
|
638
|
+
cnStrength: false,
|
|
639
|
+
cnGuidanceEnd: false
|
|
634
640
|
};
|
|
635
641
|
|
|
636
642
|
// Parse CLI args
|
|
@@ -762,6 +768,15 @@ for (let i = 0; i < args.length; i++) {
|
|
|
762
768
|
} else if (arg === '-c' || arg === '--context') {
|
|
763
769
|
options.contextImages.push(args[++i]);
|
|
764
770
|
cliSet.context = true;
|
|
771
|
+
} else if (arg === '--photobooth') {
|
|
772
|
+
options.photobooth = true;
|
|
773
|
+
cliSet.photobooth = true;
|
|
774
|
+
} else if (arg === '--cn-strength') {
|
|
775
|
+
options.cnStrength = parseFloat(args[++i]);
|
|
776
|
+
cliSet.cnStrength = true;
|
|
777
|
+
} else if (arg === '--cn-guidance-end') {
|
|
778
|
+
options.cnGuidanceEnd = parseFloat(args[++i]);
|
|
779
|
+
cliSet.cnGuidanceEnd = true;
|
|
765
780
|
} else if (arg === '--last-image') {
|
|
766
781
|
// Use image from last render as reference/context
|
|
767
782
|
if (existsSync(LAST_RENDER_PATH)) {
|
|
@@ -830,6 +845,12 @@ Image Options:
|
|
|
830
845
|
-c, --context <path> Context image for editing (can use multiple)
|
|
831
846
|
--last-image Use last generated image as context
|
|
832
847
|
|
|
848
|
+
Photobooth (Face Transfer):
|
|
849
|
+
--photobooth Face transfer mode (InstantID + SDXL Turbo)
|
|
850
|
+
--ref <path|url> Face image (required with --photobooth)
|
|
851
|
+
--cn-strength <n> ControlNet strength (default: 0.8)
|
|
852
|
+
--cn-guidance-end <n> ControlNet guidance end point (default: 0.3)
|
|
853
|
+
|
|
833
854
|
Video Options:
|
|
834
855
|
--video, -v Generate video instead of image
|
|
835
856
|
--workflow <type> Video workflow: t2v|i2v|s2v|animate-move|animate-replace
|
|
@@ -885,6 +906,8 @@ Examples:
|
|
|
885
906
|
sogni-gen --video --last-image "gentle camera pan"
|
|
886
907
|
sogni-gen -c photo.jpg "make the background a beach" -m qwen_image_edit_2511_fp8
|
|
887
908
|
sogni-gen -c subject.jpg -c style.jpg "apply the style to the subject"
|
|
909
|
+
sogni-gen --photobooth --ref face.jpg "80s fashion portrait"
|
|
910
|
+
sogni-gen --photobooth --ref face.jpg -n 4 "LinkedIn professional headshot"
|
|
888
911
|
`);
|
|
889
912
|
process.exit(0);
|
|
890
913
|
} else if (!arg.startsWith('-') && !options.prompt) {
|
|
@@ -1142,6 +1165,8 @@ if (options._lastImagePath) {
|
|
|
1142
1165
|
} else if (!options.quiet) {
|
|
1143
1166
|
console.error('Warning: --last-image ignored for text-to-video workflow.');
|
|
1144
1167
|
}
|
|
1168
|
+
} else if (options.photobooth) {
|
|
1169
|
+
if (!options.refImage) options.refImage = options._lastImagePath;
|
|
1145
1170
|
} else {
|
|
1146
1171
|
options.contextImages.push(options._lastImagePath);
|
|
1147
1172
|
}
|
|
@@ -1156,6 +1181,14 @@ if (options.video) {
|
|
|
1156
1181
|
if (!cliSet.timeout && !timeoutFromConfig && options.timeout === 30000) {
|
|
1157
1182
|
options.timeout = 300000; // 5 min for video
|
|
1158
1183
|
}
|
|
1184
|
+
} else if (options.photobooth) {
|
|
1185
|
+
// Photobooth uses SDXL Turbo + InstantID ControlNet
|
|
1186
|
+
options.model = options.model || openclawConfig?.defaultPhotoboothModel || 'coreml-sogniXLturbo_alpha1_ad';
|
|
1187
|
+
if (!cliSet.width) options.width = 1024;
|
|
1188
|
+
if (!cliSet.height) options.height = 1024;
|
|
1189
|
+
if (!cliSet.timeout && !timeoutFromConfig && options.timeout === 30000) {
|
|
1190
|
+
options.timeout = 60000;
|
|
1191
|
+
}
|
|
1159
1192
|
} else if (options.contextImages.length > 0) {
|
|
1160
1193
|
// Use qwen edit model when context images provided (unless model explicitly set)
|
|
1161
1194
|
options.model = options.model || openclawConfig?.defaultEditModel || 'qwen_image_edit_2511_fp8_lightning';
|
|
@@ -1176,6 +1209,18 @@ if (!options.video && (options.refAudio || options.refVideo || options.videoWork
|
|
|
1176
1209
|
});
|
|
1177
1210
|
}
|
|
1178
1211
|
|
|
1212
|
+
if (options.photobooth) {
|
|
1213
|
+
if (!options.refImage) {
|
|
1214
|
+
fatalCliError('--photobooth requires --ref <face-image>.', { code: 'INVALID_ARGUMENT' });
|
|
1215
|
+
}
|
|
1216
|
+
if (options.video) {
|
|
1217
|
+
fatalCliError('--photobooth cannot be combined with --video.', { code: 'INVALID_ARGUMENT' });
|
|
1218
|
+
}
|
|
1219
|
+
if (options.contextImages.length > 0) {
|
|
1220
|
+
fatalCliError('--photobooth cannot be combined with -c/--context.', { code: 'INVALID_ARGUMENT' });
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1179
1224
|
if (options.video) {
|
|
1180
1225
|
if (options.videoWorkflow === 't2v') {
|
|
1181
1226
|
if (options.refImage || options.refImageEnd || options.refAudio || options.refVideo) {
|
|
@@ -2416,6 +2461,53 @@ async function main() {
|
|
|
2416
2461
|
}
|
|
2417
2462
|
|
|
2418
2463
|
await client.createImageEditProject(editConfig);
|
|
2464
|
+
} else if (options.photobooth) {
|
|
2465
|
+
// Photobooth: face transfer with InstantID ControlNet
|
|
2466
|
+
log(`Photobooth with ${options.model}...`);
|
|
2467
|
+
if (options.seed !== null && options.seed !== undefined) log(`Using seed: ${options.seed}`);
|
|
2468
|
+
|
|
2469
|
+
const faceBuffer = await fetchMediaBuffer(options.refImage);
|
|
2470
|
+
const modelDefaults = getModelDefaults(options.model, openclawConfig);
|
|
2471
|
+
const steps = options.steps ?? modelDefaults?.steps ?? 7;
|
|
2472
|
+
const guidance = options.guidance ?? modelDefaults?.guidance ?? 2;
|
|
2473
|
+
|
|
2474
|
+
const projectConfig = {
|
|
2475
|
+
modelId: options.model,
|
|
2476
|
+
positivePrompt: options.prompt,
|
|
2477
|
+
negativePrompt: '',
|
|
2478
|
+
stylePrompt: '',
|
|
2479
|
+
numberOfMedia: options.count,
|
|
2480
|
+
tokenType: options.tokenType || 'spark',
|
|
2481
|
+
waitForCompletion: false,
|
|
2482
|
+
sizePreset: 'custom',
|
|
2483
|
+
width: options.width,
|
|
2484
|
+
height: options.height,
|
|
2485
|
+
steps,
|
|
2486
|
+
guidance,
|
|
2487
|
+
disableNSFWFilter: true,
|
|
2488
|
+
sampler: options.sampler || 'dpmpp_sde',
|
|
2489
|
+
scheduler: options.scheduler || 'karras',
|
|
2490
|
+
controlNet: {
|
|
2491
|
+
name: 'instantid',
|
|
2492
|
+
image: faceBuffer,
|
|
2493
|
+
strength: options.cnStrength ?? 0.7,
|
|
2494
|
+
mode: 'balanced',
|
|
2495
|
+
guidanceStart: 0,
|
|
2496
|
+
guidanceEnd: options.cnGuidanceEnd ?? 0.6,
|
|
2497
|
+
}
|
|
2498
|
+
};
|
|
2499
|
+
|
|
2500
|
+
if (options.outputFormat) projectConfig.outputFormat = options.outputFormat;
|
|
2501
|
+
if (options.seed !== null && options.seed !== undefined) projectConfig.seed = options.seed;
|
|
2502
|
+
if (options.loras.length > 0) projectConfig.loras = options.loras;
|
|
2503
|
+
if (options.loraStrengths.length > 0) projectConfig.loraStrengths = options.loraStrengths;
|
|
2504
|
+
|
|
2505
|
+
const projectResult = await client.createImageProject(projectConfig);
|
|
2506
|
+
|
|
2507
|
+
// Check for errors in the response (e.g., insufficient tokens)
|
|
2508
|
+
if (projectResult?.error || projectResult?.message) {
|
|
2509
|
+
throw new Error(projectResult.error || projectResult.message);
|
|
2510
|
+
}
|
|
2419
2511
|
} else {
|
|
2420
2512
|
// Standard image generation
|
|
2421
2513
|
log(`Generating with ${options.model}...`);
|
|
@@ -2513,6 +2605,10 @@ async function main() {
|
|
|
2513
2605
|
if (options.contextImages.length > 0) {
|
|
2514
2606
|
renderInfo.contextImages = options.contextImages;
|
|
2515
2607
|
}
|
|
2608
|
+
if (options.photobooth) {
|
|
2609
|
+
renderInfo.photobooth = true;
|
|
2610
|
+
renderInfo.refImage = options.refImage;
|
|
2611
|
+
}
|
|
2516
2612
|
saveLastRender(renderInfo);
|
|
2517
2613
|
|
|
2518
2614
|
// Save to file if requested
|
|
@@ -2700,6 +2796,15 @@ async function main() {
|
|
|
2700
2796
|
if (options.contextImages.length > 0) {
|
|
2701
2797
|
output.contextImages = options.contextImages;
|
|
2702
2798
|
}
|
|
2799
|
+
if (options.photobooth) {
|
|
2800
|
+
output.photobooth = true;
|
|
2801
|
+
output.refImage = options.refImage;
|
|
2802
|
+
output.controlNet = {
|
|
2803
|
+
name: 'instantid',
|
|
2804
|
+
strength: options.cnStrength ?? 0.7,
|
|
2805
|
+
guidanceEnd: options.cnGuidanceEnd ?? 0.6,
|
|
2806
|
+
};
|
|
2807
|
+
}
|
|
2703
2808
|
console.log(JSON.stringify(output));
|
|
2704
2809
|
} else {
|
|
2705
2810
|
urls.forEach(url => console.log(url));
|