sogni-gen 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/SKILL.md +194 -0
- package/package.json +25 -0
- package/screenshot.jpg +0 -0
- package/sogni-gen.mjs +501 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mauvis Ledford
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="screenshot.jpg" alt="Telegram image render workflow" width="320" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# Image & Video gen for 🦞 Moltbot
|
|
6
|
+
|
|
7
|
+
🎨 Generate **images and videos** using [Sogni AI](https://sogni.ai)'s decentralized GPU network.
|
|
8
|
+
|
|
9
|
+
A [Clawdbot](https://github.com/clawdbot/clawdbot) skill for AI image + video generation.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
### As a Clawdbot Skill
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Clone to your skills directory
|
|
17
|
+
git clone https://github.com/mauvis/sogni-gen ~/.clawdbot/skills/sogni-gen
|
|
18
|
+
cd ~/.clawdbot/skills/sogni-gen
|
|
19
|
+
npm install
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Standalone
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
git clone https://github.com/mauvis/sogni-gen
|
|
26
|
+
cd sogni-gen
|
|
27
|
+
npm install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Setup
|
|
31
|
+
|
|
32
|
+
1. Create a Sogni account at https://sogni.ai
|
|
33
|
+
2. Create credentials file:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
mkdir -p ~/.config/sogni
|
|
37
|
+
cat > ~/.config/sogni/credentials << 'EOF'
|
|
38
|
+
SOGNI_USERNAME=your_username
|
|
39
|
+
SOGNI_PASSWORD=your_password
|
|
40
|
+
EOF
|
|
41
|
+
chmod 600 ~/.config/sogni/credentials
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Generate image, get URL
|
|
48
|
+
node sogni-gen.mjs "a dragon eating tacos"
|
|
49
|
+
|
|
50
|
+
# Save to file
|
|
51
|
+
node sogni-gen.mjs -o dragon.png "a dragon eating tacos"
|
|
52
|
+
|
|
53
|
+
# JSON output
|
|
54
|
+
node sogni-gen.mjs --json "a dragon eating tacos"
|
|
55
|
+
|
|
56
|
+
# Different model
|
|
57
|
+
node sogni-gen.mjs -m flux1-schnell-fp8 "a dragon eating tacos"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Options
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
-o, --output <path> Save image to file
|
|
64
|
+
-m, --model <id> Model (default: z_image_turbo_bf16)
|
|
65
|
+
-w, --width <px> Width (default: 512)
|
|
66
|
+
-h, --height <px> Height (default: 512)
|
|
67
|
+
-n, --count <num> Number of images (default: 1)
|
|
68
|
+
-t, --timeout <sec> Timeout (default: 30)
|
|
69
|
+
--json JSON output
|
|
70
|
+
-q, --quiet Suppress progress
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Models
|
|
74
|
+
|
|
75
|
+
| Model | Speed | Notes |
|
|
76
|
+
|-------|-------|-------|
|
|
77
|
+
| `z_image_turbo_bf16` | ~5-10s | Default, general purpose |
|
|
78
|
+
| `flux1-schnell-fp8` | ~3-5s | Fast iterations |
|
|
79
|
+
| `flux2_dev_fp8` | ~2min | High quality |
|
|
80
|
+
| `chroma-v.46-flash_fp8` | ~30s | Balanced |
|
|
81
|
+
|
|
82
|
+
## With Clawdbot
|
|
83
|
+
|
|
84
|
+
Once installed, just ask your agent:
|
|
85
|
+
|
|
86
|
+
> "Draw me a picture of a slothicorn eating a banana"
|
|
87
|
+
|
|
88
|
+
The agent will generate the image and send it to your chat.
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sogni-gen
|
|
3
|
+
description: Generate images **and videos** using Sogni AI's decentralized network. Ask the agent to "draw", "generate", "create an image", or "make a video/animate" from a prompt or reference image.
|
|
4
|
+
homepage: https://sogni.ai
|
|
5
|
+
metadata:
|
|
6
|
+
clawdbot:
|
|
7
|
+
emoji: "🎨"
|
|
8
|
+
os: ["darwin", "linux", "win32"]
|
|
9
|
+
requires:
|
|
10
|
+
bins: ["node"]
|
|
11
|
+
install:
|
|
12
|
+
- id: npm
|
|
13
|
+
kind: exec
|
|
14
|
+
command: "cd {{skillDir}} && npm install"
|
|
15
|
+
label: "Install dependencies"
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Sogni Image & Video Generation
|
|
19
|
+
|
|
20
|
+
Generate **images and videos** using Sogni AI's decentralized GPU network.
|
|
21
|
+
|
|
22
|
+
## Setup
|
|
23
|
+
|
|
24
|
+
1. **Get Sogni credentials** at https://sogni.ai
|
|
25
|
+
2. **Create credentials file:**
|
|
26
|
+
```bash
|
|
27
|
+
mkdir -p ~/.config/sogni
|
|
28
|
+
cat > ~/.config/sogni/credentials << 'EOF'
|
|
29
|
+
SOGNI_USERNAME=your_username
|
|
30
|
+
SOGNI_PASSWORD=your_password
|
|
31
|
+
EOF
|
|
32
|
+
chmod 600 ~/.config/sogni/credentials
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
3. **Install dependencies:**
|
|
36
|
+
```bash
|
|
37
|
+
cd /path/to/sogni-gen
|
|
38
|
+
npm install
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage (Images & Video)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Generate and get URL
|
|
45
|
+
node sogni-gen.mjs "a cat wearing a hat"
|
|
46
|
+
|
|
47
|
+
# Save to file
|
|
48
|
+
node sogni-gen.mjs -o /tmp/cat.png "a cat wearing a hat"
|
|
49
|
+
|
|
50
|
+
# JSON output (for scripting)
|
|
51
|
+
node sogni-gen.mjs --json "a cat wearing a hat"
|
|
52
|
+
|
|
53
|
+
# Quiet mode (suppress progress)
|
|
54
|
+
node sogni-gen.mjs -q -o /tmp/cat.png "a cat wearing a hat"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Options
|
|
58
|
+
|
|
59
|
+
| Flag | Description | Default |
|
|
60
|
+
|------|-------------|---------|
|
|
61
|
+
| `-o, --output <path>` | Save to file | prints URL |
|
|
62
|
+
| `-m, --model <id>` | Model ID | z_image_turbo_bf16 |
|
|
63
|
+
| `-w, --width <px>` | Width | 512 |
|
|
64
|
+
| `-h, --height <px>` | Height | 512 |
|
|
65
|
+
| `-n, --count <num>` | Number of images | 1 |
|
|
66
|
+
| `-t, --timeout <sec>` | Timeout seconds | 30 (300 for video) |
|
|
67
|
+
| `-s, --seed <num>` | Specific seed | random |
|
|
68
|
+
| `--last-seed` | Reuse seed from last render | - |
|
|
69
|
+
| `-c, --context <path>` | Context image for editing | - |
|
|
70
|
+
| `--last-image` | Use last generated image as context/ref | - |
|
|
71
|
+
| `--video, -v` | Generate video instead of image | - |
|
|
72
|
+
| `--fps <num>` | Frames per second (video) | 16 |
|
|
73
|
+
| `--duration <sec>` | Duration in seconds (video) | 5 |
|
|
74
|
+
| `--ref <path>` | Reference image for video | required for video |
|
|
75
|
+
| `--last` | Show last render info | - |
|
|
76
|
+
| `--json` | JSON output | false |
|
|
77
|
+
| `-q, --quiet` | No progress output | false |
|
|
78
|
+
|
|
79
|
+
## Image Models
|
|
80
|
+
|
|
81
|
+
| Model | Speed | Use Case |
|
|
82
|
+
|-------|-------|----------|
|
|
83
|
+
| `z_image_turbo_bf16` | Fast (~5-10s) | General purpose, default |
|
|
84
|
+
| `flux1-schnell-fp8` | Very fast | Quick iterations |
|
|
85
|
+
| `flux2_dev_fp8` | Slow (~2min) | High quality |
|
|
86
|
+
| `chroma-v.46-flash_fp8` | Medium | Balanced |
|
|
87
|
+
| `qwen_image_edit_2511_fp8` | Medium | Image editing with context (up to 3) |
|
|
88
|
+
| `qwen_image_edit_2511_fp8_lightning` | Fast | Quick image editing |
|
|
89
|
+
|
|
90
|
+
## Video Models
|
|
91
|
+
|
|
92
|
+
| Model | Speed | Use Case |
|
|
93
|
+
|-------|-------|----------|
|
|
94
|
+
| `wan_v2.2-14b-fp8_i2v_lightx2v` | Fast | Default video generation |
|
|
95
|
+
| `wan_v2.2-14b-fp8_i2v` | Slow | Higher quality video |
|
|
96
|
+
|
|
97
|
+
## Image Editing with Context
|
|
98
|
+
|
|
99
|
+
Edit images using reference images (Qwen models support up to 3):
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Single context image
|
|
103
|
+
node sogni-gen.mjs -c photo.jpg "make the background a beach"
|
|
104
|
+
|
|
105
|
+
# Multiple context images (subject + style)
|
|
106
|
+
node sogni-gen.mjs -c subject.jpg -c style.jpg "apply the style to the subject"
|
|
107
|
+
|
|
108
|
+
# Use last generated image as context
|
|
109
|
+
node sogni-gen.mjs --last-image "make it more vibrant"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
When context images are provided without `-m`, defaults to `qwen_image_edit_2511_fp8_lightning`.
|
|
113
|
+
|
|
114
|
+
## Video Generation
|
|
115
|
+
|
|
116
|
+
Generate videos from a reference image:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# Basic video from image
|
|
120
|
+
node sogni-gen.mjs --video --ref cat.jpg -o cat.mp4 "cat walks around"
|
|
121
|
+
|
|
122
|
+
# Use last generated image as reference
|
|
123
|
+
node sogni-gen.mjs --last-image --video "gentle camera pan"
|
|
124
|
+
|
|
125
|
+
# Custom duration and FPS
|
|
126
|
+
node sogni-gen.mjs --video --ref scene.png --duration 10 --fps 24 "zoom out slowly"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Photo Restoration
|
|
130
|
+
|
|
131
|
+
Restore damaged vintage photos using Qwen image editing:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# Basic restoration
|
|
135
|
+
sogni-gen -c damaged_photo.jpg -o restored.png \
|
|
136
|
+
"professionally restore this vintage photograph, remove damage and scratches"
|
|
137
|
+
|
|
138
|
+
# Detailed restoration with preservation hints
|
|
139
|
+
sogni-gen -c old_photo.jpg -o restored.png -w 1024 -h 1280 \
|
|
140
|
+
"restore this vintage photo, remove peeling, tears and wear marks, \
|
|
141
|
+
preserve natural features and expression, maintain warm nostalgic color tones"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Tips for good restorations:**
|
|
145
|
+
- Describe the damage: "peeling", "scratches", "tears", "fading"
|
|
146
|
+
- Specify what to preserve: "natural features", "eye color", "hair", "expression"
|
|
147
|
+
- Mention the era for color tones: "1970s warm tones", "vintage sepia"
|
|
148
|
+
|
|
149
|
+
**Finding received images (Telegram/etc):**
|
|
150
|
+
```bash
|
|
151
|
+
ls -la ~/.clawdbot/media/inbound/*.jpg | tail -3
|
|
152
|
+
cp ~/.clawdbot/media/inbound/<latest>.jpg /tmp/to_restore.jpg
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Agent Usage
|
|
156
|
+
|
|
157
|
+
When user asks to generate/draw/create an image:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# Generate and save locally
|
|
161
|
+
node {{skillDir}}/sogni-gen.mjs -q -o /tmp/generated.png "user's prompt"
|
|
162
|
+
|
|
163
|
+
# Edit an existing image
|
|
164
|
+
node {{skillDir}}/sogni-gen.mjs -q -c /path/to/input.jpg -o /tmp/edited.png "make it pop art style"
|
|
165
|
+
|
|
166
|
+
# Generate video from image
|
|
167
|
+
node {{skillDir}}/sogni-gen.mjs -q --video --ref /path/to/image.png -o /tmp/video.mp4 "camera slowly zooms in"
|
|
168
|
+
|
|
169
|
+
# Then send via message tool with filePath
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## JSON Output
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"success": true,
|
|
177
|
+
"prompt": "a cat wearing a hat",
|
|
178
|
+
"model": "z_image_turbo_bf16",
|
|
179
|
+
"width": 512,
|
|
180
|
+
"height": 512,
|
|
181
|
+
"urls": ["https://..."],
|
|
182
|
+
"localPath": "/tmp/cat.png"
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Cost
|
|
187
|
+
|
|
188
|
+
Uses Spark tokens from your Sogni account. 512x512 images are most cost-efficient.
|
|
189
|
+
|
|
190
|
+
## Troubleshooting
|
|
191
|
+
|
|
192
|
+
- **Auth errors**: Check credentials in `~/.config/sogni/credentials`
|
|
193
|
+
- **Timeouts**: Try a faster model or increase `-t` timeout
|
|
194
|
+
- **No workers**: Check https://sogni.ai for network status
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sogni-gen",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Sogni AI image generation skill for Clawdbot",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "sogni-gen.mjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"sogni-gen": "./sogni-gen.mjs"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node sogni-gen.mjs --help"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"sogni",
|
|
15
|
+
"ai",
|
|
16
|
+
"image-generation",
|
|
17
|
+
"clawdbot",
|
|
18
|
+
"skill"
|
|
19
|
+
],
|
|
20
|
+
"author": "Mauvis Ledford",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@sogni-ai/sogni-client-wrapper": "^1.3.2"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/screenshot.jpg
ADDED
|
Binary file
|
package/sogni-gen.mjs
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* sogni-gen - Generate images and videos using Sogni AI
|
|
4
|
+
* Usage: sogni-gen [options] "prompt"
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { SogniClientWrapper, ClientEvent, supportsContextImages, getMaxContextImages } from '@sogni-ai/sogni-client-wrapper';
|
|
8
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
9
|
+
import { join, dirname } from 'path';
|
|
10
|
+
import { homedir } from 'os';
|
|
11
|
+
|
|
12
|
+
const LAST_RENDER_PATH = join(homedir(), '.config', 'sogni', 'last-render.json');
|
|
13
|
+
|
|
14
|
+
// Parse arguments
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
const options = {
|
|
17
|
+
prompt: null,
|
|
18
|
+
output: null,
|
|
19
|
+
model: null, // Will be set based on type
|
|
20
|
+
width: 512,
|
|
21
|
+
height: 512,
|
|
22
|
+
count: 1,
|
|
23
|
+
json: false,
|
|
24
|
+
quiet: false,
|
|
25
|
+
timeout: 30000,
|
|
26
|
+
seed: null,
|
|
27
|
+
lastSeed: false,
|
|
28
|
+
video: false,
|
|
29
|
+
fps: 16,
|
|
30
|
+
duration: 5,
|
|
31
|
+
refImage: null, // Reference image for video (start frame)
|
|
32
|
+
refImageEnd: null, // End frame for video interpolation
|
|
33
|
+
contextImages: [] // Context images for image editing
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Parse CLI args
|
|
37
|
+
for (let i = 0; i < args.length; i++) {
|
|
38
|
+
const arg = args[i];
|
|
39
|
+
if (arg === '-o' || arg === '--output') {
|
|
40
|
+
options.output = args[++i];
|
|
41
|
+
} else if (arg === '-m' || arg === '--model') {
|
|
42
|
+
options.model = args[++i];
|
|
43
|
+
} else if (arg === '-w' || arg === '--width') {
|
|
44
|
+
options.width = parseInt(args[++i]);
|
|
45
|
+
} else if (arg === '-h' || arg === '--height') {
|
|
46
|
+
options.height = parseInt(args[++i]);
|
|
47
|
+
} else if (arg === '-n' || arg === '--count') {
|
|
48
|
+
options.count = parseInt(args[++i]);
|
|
49
|
+
} else if (arg === '-t' || arg === '--timeout') {
|
|
50
|
+
options.timeout = parseInt(args[++i]) * 1000;
|
|
51
|
+
} else if (arg === '-s' || arg === '--seed') {
|
|
52
|
+
options.seed = parseInt(args[++i]);
|
|
53
|
+
} else if (arg === '--last-seed' || arg === '--reseed') {
|
|
54
|
+
options.lastSeed = true;
|
|
55
|
+
} else if (arg === '--video' || arg === '-v') {
|
|
56
|
+
options.video = true;
|
|
57
|
+
} else if (arg === '--fps') {
|
|
58
|
+
options.fps = parseInt(args[++i]);
|
|
59
|
+
} else if (arg === '--duration') {
|
|
60
|
+
options.duration = parseInt(args[++i]);
|
|
61
|
+
} else if (arg === '--ref' || arg === '--reference') {
|
|
62
|
+
options.refImage = args[++i];
|
|
63
|
+
} else if (arg === '--ref-end' || arg === '--end') {
|
|
64
|
+
options.refImageEnd = args[++i];
|
|
65
|
+
} else if (arg === '-c' || arg === '--context') {
|
|
66
|
+
options.contextImages.push(args[++i]);
|
|
67
|
+
} else if (arg === '--last-image') {
|
|
68
|
+
// Use image from last render as reference/context
|
|
69
|
+
if (existsSync(LAST_RENDER_PATH)) {
|
|
70
|
+
const lastRender = JSON.parse(readFileSync(LAST_RENDER_PATH, 'utf8'));
|
|
71
|
+
let lastImagePath = null;
|
|
72
|
+
if (lastRender.localPath && existsSync(lastRender.localPath)) {
|
|
73
|
+
lastImagePath = lastRender.localPath;
|
|
74
|
+
} else if (lastRender.urls?.[0]) {
|
|
75
|
+
lastImagePath = lastRender.urls[0];
|
|
76
|
+
}
|
|
77
|
+
if (lastImagePath) {
|
|
78
|
+
// Will be resolved later: video uses refImage, image editing uses contextImages
|
|
79
|
+
options._lastImagePath = lastImagePath;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} else if (arg === '--last') {
|
|
83
|
+
// Show last render info
|
|
84
|
+
if (existsSync(LAST_RENDER_PATH)) {
|
|
85
|
+
console.log(readFileSync(LAST_RENDER_PATH, 'utf8'));
|
|
86
|
+
} else {
|
|
87
|
+
console.error('No previous render found.');
|
|
88
|
+
}
|
|
89
|
+
process.exit(0);
|
|
90
|
+
} else if (arg === '--json') {
|
|
91
|
+
options.json = true;
|
|
92
|
+
} else if (arg === '-q' || arg === '--quiet') {
|
|
93
|
+
options.quiet = true;
|
|
94
|
+
} else if (arg === '--help') {
|
|
95
|
+
console.log(`
|
|
96
|
+
sogni-gen - Generate images and videos using Sogni AI
|
|
97
|
+
|
|
98
|
+
Usage: sogni-gen [options] "prompt"
|
|
99
|
+
|
|
100
|
+
Image Options:
|
|
101
|
+
-o, --output <path> Save to file (otherwise prints URL)
|
|
102
|
+
-m, --model <id> Model (default: z_image_turbo_bf16)
|
|
103
|
+
-w, --width <px> Width (default: 512)
|
|
104
|
+
-h, --height <px> Height (default: 512)
|
|
105
|
+
-n, --count <num> Number of images (default: 1)
|
|
106
|
+
-s, --seed <num> Use specific seed
|
|
107
|
+
--last-seed Reuse seed from previous render
|
|
108
|
+
-c, --context <path> Context image for editing (can use multiple)
|
|
109
|
+
--last-image Use last generated image as context
|
|
110
|
+
|
|
111
|
+
Video Options:
|
|
112
|
+
--video, -v Generate video instead of image
|
|
113
|
+
--fps <num> Frames per second (default: 16)
|
|
114
|
+
--duration <sec> Duration in seconds (default: 5)
|
|
115
|
+
--ref <path|url> Reference image for video (start frame)
|
|
116
|
+
--ref-end <path|url> End frame for interpolation/morphing
|
|
117
|
+
--last-image Use last generated image as reference
|
|
118
|
+
|
|
119
|
+
General:
|
|
120
|
+
-t, --timeout <sec> Timeout in seconds (default: 30, video: 300)
|
|
121
|
+
--last Show last render info (JSON)
|
|
122
|
+
--json Output JSON with all details
|
|
123
|
+
-q, --quiet Suppress progress output
|
|
124
|
+
|
|
125
|
+
Image Models:
|
|
126
|
+
z_image_turbo_bf16 Fast, general purpose (default)
|
|
127
|
+
flux1-schnell-fp8 Very fast
|
|
128
|
+
flux2_dev_fp8 High quality (slow)
|
|
129
|
+
qwen_image_edit_2511_fp8 Image editing with context (up to 3 images)
|
|
130
|
+
qwen_image_edit_2511_fp8_lightning Fast image editing
|
|
131
|
+
|
|
132
|
+
Video Models:
|
|
133
|
+
wan_v2.2-14b-fp8_i2v_lightx2v Fast (default)
|
|
134
|
+
wan_v2.2-14b-fp8_i2v Higher quality
|
|
135
|
+
|
|
136
|
+
Examples:
|
|
137
|
+
sogni-gen "a cat wearing a hat"
|
|
138
|
+
sogni-gen -o cat.jpg "a cat"
|
|
139
|
+
sogni-gen --video --ref cat.jpg -o cat.mp4 "cat walks around"
|
|
140
|
+
sogni-gen --video --last-image "gentle camera pan"
|
|
141
|
+
sogni-gen -c photo.jpg "make the background a beach" -m qwen_image_edit_2511_fp8
|
|
142
|
+
sogni-gen -c subject.jpg -c style.jpg "apply the style to the subject"
|
|
143
|
+
`);
|
|
144
|
+
process.exit(0);
|
|
145
|
+
} else if (!arg.startsWith('-') && !options.prompt) {
|
|
146
|
+
options.prompt = arg;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Resolve --last-image: video uses refImage, image uses contextImages
|
|
151
|
+
if (options._lastImagePath) {
|
|
152
|
+
if (options.video) {
|
|
153
|
+
options.refImage = options._lastImagePath;
|
|
154
|
+
} else {
|
|
155
|
+
options.contextImages.push(options._lastImagePath);
|
|
156
|
+
}
|
|
157
|
+
delete options._lastImagePath;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Set defaults based on type and context
|
|
161
|
+
if (options.video) {
|
|
162
|
+
options.model = options.model || 'wan_v2.2-14b-fp8_i2v_lightx2v';
|
|
163
|
+
options.timeout = options.timeout === 30000 ? 300000 : options.timeout; // 5 min for video
|
|
164
|
+
} else if (options.contextImages.length > 0) {
|
|
165
|
+
// Use qwen edit model when context images provided (unless model explicitly set)
|
|
166
|
+
options.model = options.model || 'qwen_image_edit_2511_fp8_lightning';
|
|
167
|
+
options.timeout = options.timeout === 30000 ? 60000 : options.timeout; // 1 min for editing
|
|
168
|
+
} else {
|
|
169
|
+
options.model = options.model || 'z_image_turbo_bf16';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!options.prompt) {
|
|
173
|
+
console.error('Error: No prompt provided. Use --help for usage.');
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (options.video && !options.refImage) {
|
|
178
|
+
console.error('Error: Video generation requires a reference image (--ref or --last-image)');
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Validate context images against model limits
|
|
183
|
+
if (options.contextImages.length > 0 && !options.video) {
|
|
184
|
+
const maxImages = getMaxContextImages(options.model);
|
|
185
|
+
if (maxImages === 0) {
|
|
186
|
+
console.error(`Error: Model ${options.model} does not support context images.`);
|
|
187
|
+
console.error('Try: qwen_image_edit_2511_fp8 or qwen_image_edit_2511_fp8_lightning');
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
if (options.contextImages.length > maxImages) {
|
|
191
|
+
console.error(`Error: Model ${options.model} supports max ${maxImages} context images, got ${options.contextImages.length}`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Load last render seed if requested
|
|
197
|
+
if (options.lastSeed) {
|
|
198
|
+
if (existsSync(LAST_RENDER_PATH)) {
|
|
199
|
+
try {
|
|
200
|
+
const lastRender = JSON.parse(readFileSync(LAST_RENDER_PATH, 'utf8'));
|
|
201
|
+
if (lastRender.seed) {
|
|
202
|
+
options.seed = lastRender.seed;
|
|
203
|
+
if (!options.quiet) console.error(`Using seed from last render: ${options.seed}`);
|
|
204
|
+
}
|
|
205
|
+
} catch (e) {
|
|
206
|
+
console.error('Warning: Could not load last render seed');
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
console.error('Warning: No previous render found, using random seed');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Load credentials
|
|
214
|
+
function loadCredentials() {
|
|
215
|
+
const credPath = join(homedir(), '.config', 'sogni', 'credentials');
|
|
216
|
+
|
|
217
|
+
if (existsSync(credPath)) {
|
|
218
|
+
const content = readFileSync(credPath, 'utf8');
|
|
219
|
+
const creds = {};
|
|
220
|
+
for (const line of content.split('\n')) {
|
|
221
|
+
const [key, val] = line.split('=');
|
|
222
|
+
if (key && val) creds[key.trim()] = val.trim();
|
|
223
|
+
}
|
|
224
|
+
if (creds.SOGNI_USERNAME && creds.SOGNI_PASSWORD) {
|
|
225
|
+
return creds;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (process.env.SOGNI_USERNAME && process.env.SOGNI_PASSWORD) {
|
|
230
|
+
return {
|
|
231
|
+
SOGNI_USERNAME: process.env.SOGNI_USERNAME,
|
|
232
|
+
SOGNI_PASSWORD: process.env.SOGNI_PASSWORD
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
console.error('Error: No Sogni credentials found.');
|
|
237
|
+
console.error('Create ~/.config/sogni/credentials with:');
|
|
238
|
+
console.error(' SOGNI_USERNAME=your_username');
|
|
239
|
+
console.error(' SOGNI_PASSWORD=your_password');
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Save last render info
|
|
244
|
+
function saveLastRender(info) {
|
|
245
|
+
try {
|
|
246
|
+
const dir = dirname(LAST_RENDER_PATH);
|
|
247
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
248
|
+
writeFileSync(LAST_RENDER_PATH, JSON.stringify(info, null, 2));
|
|
249
|
+
} catch (e) {
|
|
250
|
+
// Ignore save errors
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Fetch image as buffer
|
|
255
|
+
async function fetchImageBuffer(pathOrUrl) {
|
|
256
|
+
if (pathOrUrl.startsWith('http://') || pathOrUrl.startsWith('https://')) {
|
|
257
|
+
const response = await fetch(pathOrUrl);
|
|
258
|
+
return Buffer.from(await response.arrayBuffer());
|
|
259
|
+
} else {
|
|
260
|
+
return readFileSync(pathOrUrl);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function main() {
|
|
265
|
+
const creds = loadCredentials();
|
|
266
|
+
const log = options.quiet ? () => {} : console.error.bind(console);
|
|
267
|
+
|
|
268
|
+
log('Connecting to Sogni...');
|
|
269
|
+
|
|
270
|
+
const client = new SogniClientWrapper({
|
|
271
|
+
username: creds.SOGNI_USERNAME,
|
|
272
|
+
password: creds.SOGNI_PASSWORD,
|
|
273
|
+
network: 'fast',
|
|
274
|
+
autoConnect: false,
|
|
275
|
+
authType: 'token'
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
await client.connect();
|
|
280
|
+
log('Connected.');
|
|
281
|
+
|
|
282
|
+
const results = [];
|
|
283
|
+
let completedJobs = 0;
|
|
284
|
+
|
|
285
|
+
const completionPromise = new Promise((resolve, reject) => {
|
|
286
|
+
const timeout = setTimeout(() => {
|
|
287
|
+
reject(new Error(`Timeout after ${options.timeout / 1000}s`));
|
|
288
|
+
}, options.timeout);
|
|
289
|
+
|
|
290
|
+
client.on(ClientEvent.JOB_COMPLETED, (data) => {
|
|
291
|
+
const jobData = data.job?.data || {};
|
|
292
|
+
results.push({
|
|
293
|
+
imageUrl: data.imageUrl,
|
|
294
|
+
videoUrl: data.videoUrl,
|
|
295
|
+
seed: jobData.seed,
|
|
296
|
+
jobIndex: data.jobIndex,
|
|
297
|
+
projectId: data.projectId
|
|
298
|
+
});
|
|
299
|
+
completedJobs++;
|
|
300
|
+
log(`${options.video ? 'Video' : 'Image'} ${completedJobs}/${options.count} completed`);
|
|
301
|
+
|
|
302
|
+
if (completedJobs >= options.count) {
|
|
303
|
+
clearTimeout(timeout);
|
|
304
|
+
resolve();
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
client.on(ClientEvent.JOB_FAILED, (data) => {
|
|
309
|
+
clearTimeout(timeout);
|
|
310
|
+
reject(new Error(data.error || 'Job failed'));
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Progress for video
|
|
314
|
+
if (options.video) {
|
|
315
|
+
client.on(ClientEvent.PROJECT_PROGRESS, (data) => {
|
|
316
|
+
if (data.percentage && data.percentage > 0) {
|
|
317
|
+
log(`Progress: ${Math.round(data.percentage)}%`);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (options.video) {
|
|
324
|
+
// Video generation
|
|
325
|
+
log(`Generating video with ${options.model}...`);
|
|
326
|
+
log(`Reference: ${options.refImage}`);
|
|
327
|
+
if (options.refImageEnd) log(`End frame: ${options.refImageEnd}`);
|
|
328
|
+
|
|
329
|
+
const imageBuffer = await fetchImageBuffer(options.refImage);
|
|
330
|
+
const endImageBuffer = options.refImageEnd ? await fetchImageBuffer(options.refImageEnd) : undefined;
|
|
331
|
+
const frames = options.fps * options.duration;
|
|
332
|
+
|
|
333
|
+
const projectConfig = {
|
|
334
|
+
type: 'video',
|
|
335
|
+
modelId: options.model,
|
|
336
|
+
positivePrompt: options.prompt,
|
|
337
|
+
negativePrompt: '',
|
|
338
|
+
stylePrompt: '',
|
|
339
|
+
numberOfMedia: options.count,
|
|
340
|
+
referenceImage: imageBuffer,
|
|
341
|
+
frames: frames,
|
|
342
|
+
fps: options.fps,
|
|
343
|
+
width: options.width,
|
|
344
|
+
height: options.height,
|
|
345
|
+
tokenType: 'spark',
|
|
346
|
+
waitForCompletion: false
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// Add end frame for interpolation if provided
|
|
350
|
+
if (endImageBuffer) {
|
|
351
|
+
projectConfig.referenceImageEnd = endImageBuffer;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
await client.createProject(projectConfig);
|
|
355
|
+
} else if (options.contextImages.length > 0) {
|
|
356
|
+
// Image editing with context images
|
|
357
|
+
log(`Editing with ${options.model}...`);
|
|
358
|
+
log(`Context images: ${options.contextImages.length}`);
|
|
359
|
+
if (options.seed) log(`Using seed: ${options.seed}`);
|
|
360
|
+
|
|
361
|
+
// Load all context images as buffers
|
|
362
|
+
const contextBuffers = await Promise.all(
|
|
363
|
+
options.contextImages.map(img => fetchImageBuffer(img))
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
const editConfig = {
|
|
367
|
+
modelId: options.model,
|
|
368
|
+
positivePrompt: options.prompt,
|
|
369
|
+
contextImages: contextBuffers,
|
|
370
|
+
numberOfMedia: options.count,
|
|
371
|
+
width: options.width,
|
|
372
|
+
height: options.height,
|
|
373
|
+
steps: options.model.includes('lightning') ? 4 : 20,
|
|
374
|
+
guidance: options.model.includes('lightning') ? 3.5 : 7.5
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
if (options.seed) {
|
|
378
|
+
editConfig.seed = options.seed;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
await client.createImageEditProject(editConfig);
|
|
382
|
+
} else {
|
|
383
|
+
// Standard image generation
|
|
384
|
+
log(`Generating with ${options.model}...`);
|
|
385
|
+
if (options.seed) log(`Using seed: ${options.seed}`);
|
|
386
|
+
|
|
387
|
+
const projectConfig = {
|
|
388
|
+
type: 'image',
|
|
389
|
+
modelId: options.model,
|
|
390
|
+
positivePrompt: options.prompt,
|
|
391
|
+
negativePrompt: '',
|
|
392
|
+
stylePrompt: '',
|
|
393
|
+
numberOfImages: options.count,
|
|
394
|
+
tokenType: 'spark',
|
|
395
|
+
waitForCompletion: false,
|
|
396
|
+
sizePreset: 'custom',
|
|
397
|
+
width: options.width,
|
|
398
|
+
height: options.height,
|
|
399
|
+
guidance: 1.0
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
if (options.seed) {
|
|
403
|
+
projectConfig.seed = options.seed;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
await client.createProject(projectConfig);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Wait for completion via events
|
|
410
|
+
await completionPromise;
|
|
411
|
+
|
|
412
|
+
if (results.length > 0) {
|
|
413
|
+
const urls = results.map(r => options.video ? r.videoUrl : r.imageUrl).filter(Boolean);
|
|
414
|
+
const firstResult = results[0];
|
|
415
|
+
|
|
416
|
+
// Save last render info
|
|
417
|
+
const renderInfo = {
|
|
418
|
+
timestamp: new Date().toISOString(),
|
|
419
|
+
type: options.video ? 'video' : 'image',
|
|
420
|
+
prompt: options.prompt,
|
|
421
|
+
model: options.model,
|
|
422
|
+
width: options.width,
|
|
423
|
+
height: options.height,
|
|
424
|
+
seed: firstResult.seed,
|
|
425
|
+
seeds: results.map(r => r.seed),
|
|
426
|
+
projectId: firstResult.projectId,
|
|
427
|
+
urls: urls,
|
|
428
|
+
localPath: options.output || null
|
|
429
|
+
};
|
|
430
|
+
if (options.video) {
|
|
431
|
+
renderInfo.fps = options.fps;
|
|
432
|
+
renderInfo.duration = options.duration;
|
|
433
|
+
renderInfo.refImage = options.refImage;
|
|
434
|
+
}
|
|
435
|
+
if (options.contextImages.length > 0) {
|
|
436
|
+
renderInfo.contextImages = options.contextImages;
|
|
437
|
+
}
|
|
438
|
+
saveLastRender(renderInfo);
|
|
439
|
+
|
|
440
|
+
// Save to file if requested
|
|
441
|
+
if (options.output && urls[0]) {
|
|
442
|
+
const response = await fetch(urls[0]);
|
|
443
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
444
|
+
|
|
445
|
+
const dir = dirname(options.output);
|
|
446
|
+
if (dir && dir !== '.' && !existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
447
|
+
|
|
448
|
+
writeFileSync(options.output, buffer);
|
|
449
|
+
log(`Saved to ${options.output}`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Output result
|
|
453
|
+
if (options.json) {
|
|
454
|
+
const output = {
|
|
455
|
+
success: true,
|
|
456
|
+
type: options.video ? 'video' : 'image',
|
|
457
|
+
prompt: options.prompt,
|
|
458
|
+
model: options.model,
|
|
459
|
+
width: options.width,
|
|
460
|
+
height: options.height,
|
|
461
|
+
seed: firstResult.seed,
|
|
462
|
+
seeds: results.map(r => r.seed),
|
|
463
|
+
urls: urls,
|
|
464
|
+
localPath: options.output || null
|
|
465
|
+
};
|
|
466
|
+
if (options.video) {
|
|
467
|
+
output.fps = options.fps;
|
|
468
|
+
output.duration = options.duration;
|
|
469
|
+
}
|
|
470
|
+
if (options.contextImages.length > 0) {
|
|
471
|
+
output.contextImages = options.contextImages;
|
|
472
|
+
}
|
|
473
|
+
console.log(JSON.stringify(output));
|
|
474
|
+
} else {
|
|
475
|
+
urls.forEach(url => console.log(url));
|
|
476
|
+
}
|
|
477
|
+
} else {
|
|
478
|
+
throw new Error('No output generated - may have been filtered');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
} catch (error) {
|
|
482
|
+
if (options.json) {
|
|
483
|
+
console.log(JSON.stringify({
|
|
484
|
+
success: false,
|
|
485
|
+
error: error.message,
|
|
486
|
+
prompt: options.prompt
|
|
487
|
+
}));
|
|
488
|
+
} else {
|
|
489
|
+
console.error(`Error: ${error.message}`);
|
|
490
|
+
}
|
|
491
|
+
process.exit(1);
|
|
492
|
+
|
|
493
|
+
} finally {
|
|
494
|
+
try {
|
|
495
|
+
if (client.isConnected?.()) await client.disconnect();
|
|
496
|
+
} catch (e) {}
|
|
497
|
+
process.exit(0);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
main();
|