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 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 gen for 🦞 OpenClaw
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
- An [OpenClaw](https://github.com/OpenClaw/OpenClaw) plugin for AI image + video generation.
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
- --ref <path|url> Reference image for i2v/s2v/animate
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
- | `--ref <path>` | Reference image for video | required for video |
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.2.4",
4
- "description": "Sogni AI image generation plugin for OpenClaw",
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));