Skip to content

Commit ea83242

Browse files
AniChikageanichikagejackwener
authored
feat(yollomi): add new commands and update documentation in README files (#235)
* feat(yollomi): add new commands and update documentation in README files - Added yollomi commands for generating images, videos, and editing capabilities. - Updated README.md and README.zh-CN.md to include yollomi in the command list. - Enhanced SKILL.md with yollomi-related tags and usage examples. * feat(yollomi): add yollomi adapter to documentation - Included yollomi in the VitePress configuration for browser adapters. - Updated adapters index documentation to reflect yollomi's capabilities and commands. * fix(yollomi): bug fixes, tests & improvements - models.ts: add browser: false (no browser connection needed for hardcoded data) - edit.ts: remove unused resolveImageInput import - upload.ts: lower video upload limit from 100MB to 20MB (base64 OOM risk) - generate.ts: improve file extension detection using URL.pathname - upscale.ts: use choices for scale arg, improve extension detection - object-remover.ts: make image/mask args positional - Add yollomi models tests to public-commands.test.ts - Add yollomi generate/video graceful-failure tests to browser-auth.test.ts --------- Co-authored-by: anichikage <hanzhishuai@bytedance.com> Co-authored-by: jackwener <jakevingoo@gmail.com>
1 parent dff0fe5 commit ea83242

21 files changed

Lines changed: 962 additions & 1 deletion

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ Run `opencli list` for the live registry.
140140
| **hf** | `top` | Public |
141141
| **jike** | `feed` `search` `create` `like` `comment` `repost` `notifications` `post` `topic` `user` | Browser |
142142
| **jimeng** | `generate` `history` | Browser |
143+
| **yollomi** | `generate` `video` `edit` `upload` `models` `remove-bg` `upscale` `face-swap` `restore` `try-on` `background` `object-remover` | Browser |
143144
| **linux-do** | `hot` `latest` `search` `categories` `category` `topic` | Public |
144145
| **stackoverflow** | `hot` `search` `bounties` `unanswered` | Public |
145146
| **steam** | `top-sellers` | Public |

README.zh-CN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ npm install -g @jackwener/opencli@latest
142142
| **hf** | `top` | 公开 |
143143
| **jike** | `feed` `search` `create` `like` `comment` `repost` `notifications` `post` `topic` `user` | 浏览器 |
144144
| **jimeng** | `generate` `history` | 浏览器 |
145+
| **yollomi** | `generate` `video` `edit` `upload` `models` `remove-bg` `upscale` `face-swap` `restore` `try-on` `background` `object-remover` | 浏览器 |
145146
| **linux-do** | `hot` `latest` `search` `categories` `category` `topic` | 公开 |
146147
| **stackoverflow** | `hot` `search` `bounties` `unanswered` | 公开 |
147148
| **steam** | `top-sellers` | 公开 |

SKILL.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: opencli
33
description: "OpenCLI — Make any website or Electron App your CLI. Zero risk, AI-powered, reuse Chrome login. 150+ commands across 30+ sites."
44
version: 1.1.0
55
author: jackwener
6-
tags: [cli, browser, web, chrome-extension, cdp, bilibili, zhihu, twitter, github, v2ex, hackernews, reddit, xiaohongshu, xueqiu, youtube, boss, coupang, AI, agent]
6+
tags: [cli, browser, web, chrome-extension, cdp, bilibili, zhihu, twitter, github, v2ex, hackernews, reddit, xiaohongshu, xueqiu, youtube, boss, coupang, yollomi, AI, agent]
77
---
88

99
# OpenCLI
@@ -222,6 +222,14 @@ opencli weread ranking --limit 10 # 排行榜
222222
opencli jimeng generate --prompt "描述" # AI 生图
223223
opencli jimeng history --limit 10 # 生成历史
224224

225+
# Yollomi yollomi.com (browser — 需在 Chrome 登录 yollomi.com,复用站点 session)
226+
opencli yollomi models --type image # 列出图像模型与积分
227+
opencli yollomi generate "提示词" --model z-image-turbo # 文生图
228+
opencli yollomi video "提示词" --model kling-2-1 # 视频
229+
opencli yollomi upload ./photo.jpg # 上传得 URL,供 img2img / 工具链使用
230+
opencli yollomi remove-bg <image-url> # 去背景(免费)
231+
opencli yollomi edit <image-url> "改成油画风格" # Qwen 图像编辑
232+
225233
# Grok (default + explicit web)
226234
opencli grok ask --prompt "问题" # 提问 Grok(兼容默认路径)
227235
opencli grok ask --prompt "问题" --web # 显式 grok.com consumer web UI 路径

docs/.vitepress/config.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export default defineConfig({
6464
{ text: 'SMZDM', link: '/adapters/browser/smzdm' },
6565
{ text: 'Jike', link: '/adapters/browser/jike' },
6666
{ text: 'Jimeng', link: '/adapters/browser/jimeng' },
67+
{ text: 'Yollomi', link: '/adapters/browser/yollomi' },
6768
{ text: 'LINUX DO', link: '/adapters/browser/linux-do' },
6869
{ text: 'Chaoxing', link: '/adapters/browser/chaoxing' },
6970
{ text: 'Grok', link: '/adapters/browser/grok' },

docs/adapters/browser/yollomi.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Yollomi
2+
3+
**Mode**: 🔐 Browser · **Domain**: `yollomi.com`
4+
5+
AI image/video generation and editing on [yollomi.com](https://yollomi.com). Uses the same `/api/ai/*` routes as the web app; authentication is your **logged-in Chrome session** (NextAuth cookies).
6+
7+
## Commands
8+
9+
| Command | Description |
10+
|---------|-------------|
11+
| `opencli yollomi generate` | Text-to-image / image-to-image |
12+
| `opencli yollomi video` | Text-to-video / image-to-video |
13+
| `opencli yollomi edit` | Qwen image edit (prompt + image) |
14+
| `opencli yollomi upload` | Upload a local file → public URL for other commands |
15+
| `opencli yollomi models` | List image / video / tool models and credit costs |
16+
| `opencli yollomi remove-bg` | Remove background (free) |
17+
| `opencli yollomi upscale` | Image upscaling |
18+
| `opencli yollomi face-swap` | Face swap between two images |
19+
| `opencli yollomi restore` | Photo restoration |
20+
| `opencli yollomi try-on` | Virtual try-on |
21+
| `opencli yollomi background` | AI background for product/object images |
22+
| `opencli yollomi object-remover` | Remove objects (image + mask URLs) |
23+
24+
## Usage Examples
25+
26+
```bash
27+
# List models
28+
opencli yollomi models --type image
29+
30+
# Text-to-image (default model: z-image-turbo)
31+
opencli yollomi generate "a red apple on a wooden table"
32+
33+
# Choose model and aspect ratio
34+
opencli yollomi generate "sunset" --model flux-schnell --ratio 16:9
35+
36+
# Image-to-image: upload first, then pass URL
37+
opencli yollomi upload ./photo.png
38+
opencli yollomi generate "oil painting style" --model flux-2-pro --image "https://..."
39+
40+
# Video
41+
opencli yollomi video "waves on a beach" --model kling-2-1
42+
43+
# Tools
44+
opencli yollomi remove-bg https://example.com/image.png
45+
opencli yollomi upscale https://example.com/image.png --scale 4
46+
opencli yollomi edit https://example.com/in.png "make it vintage"
47+
```
48+
49+
### Common options
50+
51+
| Option | Applies to | Description |
52+
|--------|------------|-------------|
53+
| `--model` | `generate`, `video` | Model id (see `yollomi models`) |
54+
| `--ratio` | `generate`, `video` | Aspect ratio, e.g. `1:1`, `16:9` |
55+
| `--image` | `generate`, `video` | Image URL for img2img / i2v |
56+
| `--output` | Most | Output directory (default `./yollomi-output`) |
57+
| `--no-download` | Several | Print URLs only, skip saving files |
58+
59+
## Prerequisites
60+
61+
- Chrome running and **logged into** [yollomi.com](https://yollomi.com) (Google OAuth)
62+
- [Browser Bridge extension](/guide/browser-bridge) installed; daemon connects on first command
63+
64+
The CLI ensures the automation tab is on `yollomi.com` before calling APIs (same-origin `fetch` with session cookies).
65+
66+
## Notes
67+
68+
- **Credits**: Each model consumes account credits; insufficient credits returns HTTP 402.
69+
- **Upload**: Local paths for tools are not accepted directly — use `yollomi upload` to get a URL, or pass an existing HTTPS image URL.

docs/adapters/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Run `opencli list` for the live registry.
2424
| **[smzdm](/adapters/browser/smzdm)** | `search` | 🔐 Browser |
2525
| **[jike](/adapters/browser/jike)** | `feed` `search` `post` `topic` `user` `create` `comment` `like` `repost` `notifications` | 🔐 Browser |
2626
| **[jimeng](/adapters/browser/jimeng)** | `generate` `history` | 🔐 Browser |
27+
| **[yollomi](/adapters/browser/yollomi)** | `generate` `video` `edit` `upload` `models` `remove-bg` `upscale` `face-swap` `restore` `try-on` `background` `object-remover` | 🔐 Browser |
2728
| **[linux-do](/adapters/browser/linux-do)** | `hot` `latest` `categories` `category` `search` `topic` | 🔐 Browser |
2829
| **[chaoxing](/adapters/browser/chaoxing)** | `assignments` `exams` | 🔐 Browser |
2930
| **[grok](/adapters/browser/grok)** | `ask` | 🔐 Browser |

src/clis/yollomi/background.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Yollomi AI background generator — POST /api/ai/ai-background-generator
3+
*/
4+
5+
import * as path from 'node:path';
6+
import chalk from 'chalk';
7+
import { cli, Strategy } from '../../registry.js';
8+
import { CliError } from '../../errors.js';
9+
import { YOLLOMI_DOMAIN, yollomiPost, downloadOutput, fmtBytes } from './utils.js';
10+
11+
cli({
12+
site: 'yollomi',
13+
name: 'background',
14+
description: 'Generate AI background for a product/object image (5 credits)',
15+
domain: YOLLOMI_DOMAIN,
16+
strategy: Strategy.COOKIE,
17+
args: [
18+
{ name: 'image', positional: true, required: true, help: 'Image URL (upload via "opencli yollomi upload" first)' },
19+
{ name: 'prompt', default: '', help: 'Background description (optional)' },
20+
{ name: 'output', default: './yollomi-output', help: 'Output directory' },
21+
{ name: 'no-download', type: 'boolean', default: false, help: 'Only show URL' },
22+
],
23+
columns: ['status', 'file', 'size', 'url'],
24+
func: async (page, kwargs) => {
25+
const imageUrl = kwargs.image as string;
26+
const prompt = kwargs.prompt as string;
27+
28+
process.stderr.write(chalk.dim('Generating background...\n'));
29+
const data = await yollomiPost(page, '/api/ai/ai-background-generator', {
30+
images: [imageUrl],
31+
prompt: prompt || undefined,
32+
aspect_ratio: '1:1',
33+
});
34+
35+
const url = data.image || (data.images?.[0]);
36+
if (!url) throw new CliError('EMPTY_RESPONSE', 'No result', 'Try a different image');
37+
38+
if (kwargs['no-download']) return [{ status: 'generated', file: '-', size: '-', url }];
39+
40+
try {
41+
const filename = `yollomi_bg_${Date.now()}.png`;
42+
const { path: fp, size } = await downloadOutput(url, kwargs.output as string, filename);
43+
return [{ status: 'saved', file: path.relative('.', fp), size: fmtBytes(size), url }];
44+
} catch {
45+
return [{ status: 'download-failed', file: '-', size: '-', url }];
46+
}
47+
},
48+
});

src/clis/yollomi/edit.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* Yollomi image editing — POST /api/ai/qwen-image-edit
3+
* Matches frontend workspace-generator.tsx for qwen-image-edit model.
4+
*/
5+
6+
import * as path from 'node:path';
7+
import chalk from 'chalk';
8+
import { cli, Strategy } from '../../registry.js';
9+
import { CliError } from '../../errors.js';
10+
import { YOLLOMI_DOMAIN, yollomiPost, downloadOutput, fmtBytes } from './utils.js';
11+
12+
cli({
13+
site: 'yollomi',
14+
name: 'edit',
15+
description: 'Edit images with AI text prompts (Qwen image edit)',
16+
domain: YOLLOMI_DOMAIN,
17+
strategy: Strategy.COOKIE,
18+
args: [
19+
{ name: 'image', positional: true, required: true, help: 'Input image URL (upload via "opencli yollomi upload" first)' },
20+
{ name: 'prompt', positional: true, required: true, help: 'Editing instruction (e.g. "Make it look vintage")' },
21+
{ name: 'model', default: 'qwen-image-edit', choices: ['qwen-image-edit', 'qwen-image-edit-plus'], help: 'Edit model' },
22+
{ name: 'output', default: './yollomi-output', help: 'Output directory' },
23+
{ name: 'no-download', type: 'boolean', default: false, help: 'Only show URL' },
24+
],
25+
columns: ['status', 'file', 'size', 'credits', 'url'],
26+
func: async (page, kwargs) => {
27+
const imageInput = kwargs.image as string;
28+
const prompt = kwargs.prompt as string;
29+
const modelId = kwargs.model as string;
30+
31+
let body: Record<string, unknown>;
32+
if (modelId === 'qwen-image-edit-plus') {
33+
body = { prompt, images: [imageInput] };
34+
} else {
35+
body = { image: imageInput, prompt, go_fast: true, output_format: 'png' };
36+
}
37+
38+
const apiPath = modelId === 'qwen-image-edit-plus' ? '/api/ai/qwen-image-edit-plus' : '/api/ai/qwen-image-edit';
39+
process.stderr.write(chalk.dim(`Editing with ${modelId}...\n`));
40+
const data = await yollomiPost(page, apiPath, body);
41+
42+
const images: string[] = data.images || (data.image ? [data.image] : []);
43+
if (!images.length) throw new CliError('EMPTY_RESPONSE', 'No result', 'Try a different prompt');
44+
45+
const credits = data.remainingCredits;
46+
const url = images[0];
47+
if (kwargs['no-download']) return [{ status: 'edited', file: '-', size: '-', credits: credits ?? '-', url }];
48+
49+
try {
50+
const filename = `yollomi_edit_${Date.now()}.png`;
51+
const { path: fp, size } = await downloadOutput(url, kwargs.output as string, filename);
52+
if (credits !== undefined) process.stderr.write(chalk.dim(`Credits remaining: ${credits}\n`));
53+
return [{ status: 'saved', file: path.relative('.', fp), size: fmtBytes(size), credits: credits ?? '-', url }];
54+
} catch {
55+
return [{ status: 'download-failed', file: '-', size: '-', credits: credits ?? '-', url }];
56+
}
57+
},
58+
});

src/clis/yollomi/face-swap.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Yollomi face swap — POST /api/ai/face-swap
3+
* Uses swap_image / input_image field names matching the frontend.
4+
*/
5+
6+
import * as path from 'node:path';
7+
import chalk from 'chalk';
8+
import { cli, Strategy } from '../../registry.js';
9+
import { CliError } from '../../errors.js';
10+
import { YOLLOMI_DOMAIN, yollomiPost, downloadOutput, fmtBytes } from './utils.js';
11+
12+
cli({
13+
site: 'yollomi',
14+
name: 'face-swap',
15+
description: 'Swap faces between two photos (3 credits)',
16+
domain: YOLLOMI_DOMAIN,
17+
strategy: Strategy.COOKIE,
18+
args: [
19+
{ name: 'source', required: true, help: 'Source face image URL' },
20+
{ name: 'target', required: true, help: 'Target photo URL' },
21+
{ name: 'output', default: './yollomi-output', help: 'Output directory' },
22+
{ name: 'no-download', type: 'boolean', default: false, help: 'Only show URL' },
23+
],
24+
columns: ['status', 'file', 'size', 'url'],
25+
func: async (page, kwargs) => {
26+
process.stderr.write(chalk.dim('Swapping faces...\n'));
27+
const data = await yollomiPost(page, '/api/ai/face-swap', {
28+
swap_image: kwargs.source as string,
29+
input_image: kwargs.target as string,
30+
});
31+
32+
const url = data.image || (data.images?.[0]);
33+
if (!url) throw new CliError('EMPTY_RESPONSE', 'No result', 'Make sure both images contain clear faces');
34+
35+
if (kwargs['no-download']) return [{ status: 'swapped', file: '-', size: '-', url }];
36+
37+
try {
38+
const filename = `yollomi_faceswap_${Date.now()}.jpg`;
39+
const { path: fp, size } = await downloadOutput(url, kwargs.output as string, filename);
40+
return [{ status: 'saved', file: path.relative('.', fp), size: fmtBytes(size), url }];
41+
} catch {
42+
return [{ status: 'download-failed', file: '-', size: '-', url }];
43+
}
44+
},
45+
});

src/clis/yollomi/generate.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* Yollomi text-to-image / image-to-image generation.
3+
*
4+
* Uses per-model routes exactly like the frontend:
5+
* POST /api/ai/z-image-turbo { prompt, width, height, ... }
6+
* POST /api/ai/nano-banana { prompt, aspect_ratio, ... }
7+
* POST /api/ai/flux-2-pro { prompt, aspectRatio, imageUrl?, ... }
8+
*/
9+
10+
import * as path from 'node:path';
11+
import chalk from 'chalk';
12+
import { cli, Strategy } from '../../registry.js';
13+
import { CliError } from '../../errors.js';
14+
import { YOLLOMI_DOMAIN, yollomiPost, resolveImageInput, downloadOutput, fmtBytes, MODEL_ROUTES } from './utils.js';
15+
16+
function getDimensions(ratio: string): { width: number; height: number } {
17+
const map: Record<string, [number, number]> = {
18+
'1:1': [1024, 1024], '16:9': [1344, 768], '9:16': [768, 1344],
19+
'4:3': [1152, 896], '3:4': [896, 1152],
20+
};
21+
const [w, h] = map[ratio] || [1024, 1024];
22+
return { width: w, height: h };
23+
}
24+
25+
cli({
26+
site: 'yollomi',
27+
name: 'generate',
28+
description: 'Generate images with AI (text-to-image or image-to-image)',
29+
domain: YOLLOMI_DOMAIN,
30+
strategy: Strategy.COOKIE,
31+
args: [
32+
{ name: 'prompt', positional: true, required: true, help: 'Text prompt describing the image' },
33+
{ name: 'model', default: 'z-image-turbo', help: 'Model ID (z-image-turbo, flux-schnell, nano-banana, flux-2-pro, ...)' },
34+
{ name: 'ratio', default: '1:1', choices: ['1:1', '16:9', '9:16', '4:3', '3:4'], help: 'Aspect ratio' },
35+
{ name: 'image', help: 'Input image URL for image-to-image (upload via "opencli yollomi upload" first)' },
36+
{ name: 'output', default: './yollomi-output', help: 'Output directory' },
37+
{ name: 'no-download', type: 'boolean', default: false, help: 'Only show URLs, skip download' },
38+
],
39+
columns: ['index', 'status', 'file', 'size', 'url'],
40+
func: async (page, kwargs) => {
41+
const prompt = kwargs.prompt as string;
42+
const modelId = kwargs.model as string;
43+
const ratio = kwargs.ratio as string;
44+
45+
const apiPath = MODEL_ROUTES[modelId];
46+
if (!apiPath) throw new CliError('INVALID_MODEL', `Unknown model: ${modelId}`, 'Run "opencli yollomi models --type image" to see available models');
47+
48+
let body: Record<string, unknown>;
49+
50+
if (modelId === 'z-image-turbo') {
51+
const { width, height } = getDimensions(ratio);
52+
body = { prompt, width, height, output_format: 'jpg', output_quality: 85, guidance_scale: 0, num_inference_steps: 8 };
53+
} else if (modelId === 'flux-2-pro') {
54+
body = { prompt, aspectRatio: ratio, outputNumber: 1 };
55+
if (kwargs.image) body.imageUrl = kwargs.image as string;
56+
} else if (modelId === 'flux-kontext-pro') {
57+
body = { prompt, output_format: 'jpg' };
58+
if (kwargs.image) body.imageUrl = kwargs.image as string;
59+
if (ratio !== '1:1') body.aspect_ratio = ratio;
60+
} else {
61+
body = { prompt, aspect_ratio: ratio };
62+
if (kwargs.image) body.imageUrl = kwargs.image as string;
63+
}
64+
65+
process.stderr.write(chalk.dim(`Generating with ${modelId}...\n`));
66+
const data = await yollomiPost(page, apiPath, body);
67+
68+
const images: string[] = data.images || (data.image ? [data.image] : []);
69+
if (!images.length) throw new CliError('EMPTY_RESPONSE', 'No images returned', 'Try a different prompt or model');
70+
71+
const noDownload = kwargs['no-download'] as boolean;
72+
const outputDir = kwargs.output as string;
73+
const results: any[] = [];
74+
75+
for (let i = 0; i < images.length; i++) {
76+
const url = images[i];
77+
if (noDownload) {
78+
results.push({ index: i + 1, status: 'generated', file: '-', size: '-', url });
79+
continue;
80+
}
81+
try {
82+
const urlPath = (() => { try { return new URL(url).pathname; } catch { return url; } })();
83+
const ext = urlPath.endsWith('.png') || urlPath.endsWith('.webp') ? urlPath.slice(urlPath.lastIndexOf('.')) : '.jpg';
84+
const filename = `yollomi_${modelId}_${Date.now()}_${i + 1}${ext}`;
85+
const { path: fp, size } = await downloadOutput(url, outputDir, filename);
86+
results.push({ index: i + 1, status: 'saved', file: path.relative('.', fp), size: fmtBytes(size), url });
87+
} catch {
88+
results.push({ index: i + 1, status: 'download-failed', file: '-', size: '-', url });
89+
}
90+
}
91+
92+
if (data.remainingCredits !== undefined) process.stderr.write(chalk.dim(`Credits remaining: ${data.remainingCredits}\n`));
93+
return results;
94+
},
95+
});

0 commit comments

Comments
 (0)