From c17c5269652ef8305034f331f9a82ee008335fae Mon Sep 17 00:00:00 2001 From: anichikage Date: Sun, 22 Mar 2026 13:25:07 +0800 Subject: [PATCH 1/2] 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. --- README.md | 1 + README.zh-CN.md | 1 + SKILL.md | 10 +- src/clis/yollomi/background.ts | 48 +++++++ src/clis/yollomi/edit.ts | 58 +++++++++ src/clis/yollomi/face-swap.ts | 45 +++++++ src/clis/yollomi/generate.ts | 94 ++++++++++++++ src/clis/yollomi/models.ts | 37 ++++++ src/clis/yollomi/object-remover.ts | 44 +++++++ src/clis/yollomi/remove-bg.ts | 40 ++++++ src/clis/yollomi/restore.ts | 40 ++++++ src/clis/yollomi/try-on.ts | 48 +++++++ src/clis/yollomi/upload.ts | 76 +++++++++++ src/clis/yollomi/upscale.ts | 48 +++++++ src/clis/yollomi/utils.ts | 202 +++++++++++++++++++++++++++++ src/clis/yollomi/video.ts | 61 +++++++++ 16 files changed, 852 insertions(+), 1 deletion(-) create mode 100644 src/clis/yollomi/background.ts create mode 100644 src/clis/yollomi/edit.ts create mode 100644 src/clis/yollomi/face-swap.ts create mode 100644 src/clis/yollomi/generate.ts create mode 100644 src/clis/yollomi/models.ts create mode 100644 src/clis/yollomi/object-remover.ts create mode 100644 src/clis/yollomi/remove-bg.ts create mode 100644 src/clis/yollomi/restore.ts create mode 100644 src/clis/yollomi/try-on.ts create mode 100644 src/clis/yollomi/upload.ts create mode 100644 src/clis/yollomi/upscale.ts create mode 100644 src/clis/yollomi/utils.ts create mode 100644 src/clis/yollomi/video.ts diff --git a/README.md b/README.md index 93598ac..7fcfd10 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,7 @@ Run `opencli list` for the live registry. | **hf** | `top` | Public | | **jike** | `feed` `search` `create` `like` `comment` `repost` `notifications` `post` `topic` `user` | Browser | | **jimeng** | `generate` `history` | Browser | +| **yollomi** | `generate` `video` `edit` `upload` `models` `remove-bg` `upscale` `face-swap` `restore` `try-on` `background` `object-remover` | Browser | | **linux-do** | `hot` `latest` `search` `categories` `category` `topic` | Public | | **stackoverflow** | `hot` `search` `bounties` `unanswered` | Public | | **steam** | `top-sellers` | Public | diff --git a/README.zh-CN.md b/README.zh-CN.md index 66d1401..6496366 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -157,6 +157,7 @@ npm install -g @jackwener/opencli@latest | **hf** | `top` | 公开 | | **jike** | `feed` `search` `create` `like` `comment` `repost` `notifications` `post` `topic` `user` | 浏览器 | | **jimeng** | `generate` `history` | 浏览器 | +| **yollomi** | `generate` `video` `edit` `upload` `models` `remove-bg` `upscale` `face-swap` `restore` `try-on` `background` `object-remover` | 浏览器 | | **linux-do** | `hot` `latest` `search` `categories` `category` `topic` | 公开 | | **stackoverflow** | `hot` `search` `bounties` `unanswered` | 公开 | | **steam** | `top-sellers` | 公开 | diff --git a/SKILL.md b/SKILL.md index f5f1fed..077baab 100644 --- a/SKILL.md +++ b/SKILL.md @@ -3,7 +3,7 @@ name: opencli description: "OpenCLI — Make any website or Electron App your CLI. Zero risk, AI-powered, reuse Chrome login. 150+ commands across 30+ sites." version: 1.1.0 author: jackwener -tags: [cli, browser, web, chrome-extension, cdp, bilibili, zhihu, twitter, github, v2ex, hackernews, reddit, xiaohongshu, xueqiu, youtube, boss, coupang, AI, agent] +tags: [cli, browser, web, chrome-extension, cdp, bilibili, zhihu, twitter, github, v2ex, hackernews, reddit, xiaohongshu, xueqiu, youtube, boss, coupang, yollomi, AI, agent] --- # OpenCLI @@ -220,6 +220,14 @@ opencli weread ranking --limit 10 # 排行榜 opencli jimeng generate --prompt "描述" # AI 生图 opencli jimeng history --limit 10 # 生成历史 +# Yollomi yollomi.com (browser — 需在 Chrome 登录 yollomi.com,复用站点 session) +opencli yollomi models --type image # 列出图像模型与积分 +opencli yollomi generate "提示词" --model z-image-turbo # 文生图 +opencli yollomi video "提示词" --model kling-2-1 # 视频 +opencli yollomi upload ./photo.jpg # 上传得 URL,供 img2img / 工具链使用 +opencli yollomi remove-bg # 去背景(免费) +opencli yollomi edit "改成油画风格" # Qwen 图像编辑 + # Grok (default + explicit web) opencli grok ask --prompt "问题" # 提问 Grok(兼容默认路径) opencli grok ask --prompt "问题" --web # 显式 grok.com consumer web UI 路径 diff --git a/src/clis/yollomi/background.ts b/src/clis/yollomi/background.ts new file mode 100644 index 0000000..538f5c5 --- /dev/null +++ b/src/clis/yollomi/background.ts @@ -0,0 +1,48 @@ +/** + * Yollomi AI background generator — POST /api/ai/ai-background-generator + */ + +import * as path from 'node:path'; +import chalk from 'chalk'; +import { cli, Strategy } from '../../registry.js'; +import { CliError } from '../../errors.js'; +import { YOLLOMI_DOMAIN, yollomiPost, downloadOutput, fmtBytes } from './utils.js'; + +cli({ + site: 'yollomi', + name: 'background', + description: 'Generate AI background for a product/object image (5 credits)', + domain: YOLLOMI_DOMAIN, + strategy: Strategy.COOKIE, + args: [ + { name: 'image', positional: true, required: true, help: 'Image URL (upload via "opencli yollomi upload" first)' }, + { name: 'prompt', default: '', help: 'Background description (optional)' }, + { name: 'output', default: './yollomi-output', help: 'Output directory' }, + { name: 'no-download', type: 'boolean', default: false, help: 'Only show URL' }, + ], + columns: ['status', 'file', 'size', 'url'], + func: async (page, kwargs) => { + const imageUrl = kwargs.image as string; + const prompt = kwargs.prompt as string; + + process.stderr.write(chalk.dim('Generating background...\n')); + const data = await yollomiPost(page, '/api/ai/ai-background-generator', { + images: [imageUrl], + prompt: prompt || undefined, + aspect_ratio: '1:1', + }); + + const url = data.image || (data.images?.[0]); + if (!url) throw new CliError('EMPTY_RESPONSE', 'No result', 'Try a different image'); + + if (kwargs['no-download']) return [{ status: 'generated', file: '-', size: '-', url }]; + + try { + const filename = `yollomi_bg_${Date.now()}.png`; + const { path: fp, size } = await downloadOutput(url, kwargs.output as string, filename); + return [{ status: 'saved', file: path.relative('.', fp), size: fmtBytes(size), url }]; + } catch { + return [{ status: 'download-failed', file: '-', size: '-', url }]; + } + }, +}); diff --git a/src/clis/yollomi/edit.ts b/src/clis/yollomi/edit.ts new file mode 100644 index 0000000..371e3ef --- /dev/null +++ b/src/clis/yollomi/edit.ts @@ -0,0 +1,58 @@ +/** + * Yollomi image editing — POST /api/ai/qwen-image-edit + * Matches frontend workspace-generator.tsx for qwen-image-edit model. + */ + +import * as path from 'node:path'; +import chalk from 'chalk'; +import { cli, Strategy } from '../../registry.js'; +import { CliError } from '../../errors.js'; +import { YOLLOMI_DOMAIN, yollomiPost, resolveImageInput, downloadOutput, fmtBytes } from './utils.js'; + +cli({ + site: 'yollomi', + name: 'edit', + description: 'Edit images with AI text prompts (Qwen image edit)', + domain: YOLLOMI_DOMAIN, + strategy: Strategy.COOKIE, + args: [ + { name: 'image', positional: true, required: true, help: 'Input image URL (upload via "opencli yollomi upload" first)' }, + { name: 'prompt', positional: true, required: true, help: 'Editing instruction (e.g. "Make it look vintage")' }, + { name: 'model', default: 'qwen-image-edit', choices: ['qwen-image-edit', 'qwen-image-edit-plus'], help: 'Edit model' }, + { name: 'output', default: './yollomi-output', help: 'Output directory' }, + { name: 'no-download', type: 'boolean', default: false, help: 'Only show URL' }, + ], + columns: ['status', 'file', 'size', 'credits', 'url'], + func: async (page, kwargs) => { + const imageInput = kwargs.image as string; + const prompt = kwargs.prompt as string; + const modelId = kwargs.model as string; + + let body: Record; + if (modelId === 'qwen-image-edit-plus') { + body = { prompt, images: [imageInput] }; + } else { + body = { image: imageInput, prompt, go_fast: true, output_format: 'png' }; + } + + const apiPath = modelId === 'qwen-image-edit-plus' ? '/api/ai/qwen-image-edit-plus' : '/api/ai/qwen-image-edit'; + process.stderr.write(chalk.dim(`Editing with ${modelId}...\n`)); + const data = await yollomiPost(page, apiPath, body); + + const images: string[] = data.images || (data.image ? [data.image] : []); + if (!images.length) throw new CliError('EMPTY_RESPONSE', 'No result', 'Try a different prompt'); + + const credits = data.remainingCredits; + const url = images[0]; + if (kwargs['no-download']) return [{ status: 'edited', file: '-', size: '-', credits: credits ?? '-', url }]; + + try { + const filename = `yollomi_edit_${Date.now()}.png`; + const { path: fp, size } = await downloadOutput(url, kwargs.output as string, filename); + if (credits !== undefined) process.stderr.write(chalk.dim(`Credits remaining: ${credits}\n`)); + return [{ status: 'saved', file: path.relative('.', fp), size: fmtBytes(size), credits: credits ?? '-', url }]; + } catch { + return [{ status: 'download-failed', file: '-', size: '-', credits: credits ?? '-', url }]; + } + }, +}); diff --git a/src/clis/yollomi/face-swap.ts b/src/clis/yollomi/face-swap.ts new file mode 100644 index 0000000..0d4ac2c --- /dev/null +++ b/src/clis/yollomi/face-swap.ts @@ -0,0 +1,45 @@ +/** + * Yollomi face swap — POST /api/ai/face-swap + * Uses swap_image / input_image field names matching the frontend. + */ + +import * as path from 'node:path'; +import chalk from 'chalk'; +import { cli, Strategy } from '../../registry.js'; +import { CliError } from '../../errors.js'; +import { YOLLOMI_DOMAIN, yollomiPost, downloadOutput, fmtBytes } from './utils.js'; + +cli({ + site: 'yollomi', + name: 'face-swap', + description: 'Swap faces between two photos (3 credits)', + domain: YOLLOMI_DOMAIN, + strategy: Strategy.COOKIE, + args: [ + { name: 'source', required: true, help: 'Source face image URL' }, + { name: 'target', required: true, help: 'Target photo URL' }, + { name: 'output', default: './yollomi-output', help: 'Output directory' }, + { name: 'no-download', type: 'boolean', default: false, help: 'Only show URL' }, + ], + columns: ['status', 'file', 'size', 'url'], + func: async (page, kwargs) => { + process.stderr.write(chalk.dim('Swapping faces...\n')); + const data = await yollomiPost(page, '/api/ai/face-swap', { + swap_image: kwargs.source as string, + input_image: kwargs.target as string, + }); + + const url = data.image || (data.images?.[0]); + if (!url) throw new CliError('EMPTY_RESPONSE', 'No result', 'Make sure both images contain clear faces'); + + if (kwargs['no-download']) return [{ status: 'swapped', file: '-', size: '-', url }]; + + try { + const filename = `yollomi_faceswap_${Date.now()}.jpg`; + const { path: fp, size } = await downloadOutput(url, kwargs.output as string, filename); + return [{ status: 'saved', file: path.relative('.', fp), size: fmtBytes(size), url }]; + } catch { + return [{ status: 'download-failed', file: '-', size: '-', url }]; + } + }, +}); diff --git a/src/clis/yollomi/generate.ts b/src/clis/yollomi/generate.ts new file mode 100644 index 0000000..e5970f6 --- /dev/null +++ b/src/clis/yollomi/generate.ts @@ -0,0 +1,94 @@ +/** + * Yollomi text-to-image / image-to-image generation. + * + * Uses per-model routes exactly like the frontend: + * POST /api/ai/z-image-turbo { prompt, width, height, ... } + * POST /api/ai/nano-banana { prompt, aspect_ratio, ... } + * POST /api/ai/flux-2-pro { prompt, aspectRatio, imageUrl?, ... } + */ + +import * as path from 'node:path'; +import chalk from 'chalk'; +import { cli, Strategy } from '../../registry.js'; +import { CliError } from '../../errors.js'; +import { YOLLOMI_DOMAIN, yollomiPost, resolveImageInput, downloadOutput, fmtBytes, MODEL_ROUTES } from './utils.js'; + +function getDimensions(ratio: string): { width: number; height: number } { + const map: Record = { + '1:1': [1024, 1024], '16:9': [1344, 768], '9:16': [768, 1344], + '4:3': [1152, 896], '3:4': [896, 1152], + }; + const [w, h] = map[ratio] || [1024, 1024]; + return { width: w, height: h }; +} + +cli({ + site: 'yollomi', + name: 'generate', + description: 'Generate images with AI (text-to-image or image-to-image)', + domain: YOLLOMI_DOMAIN, + strategy: Strategy.COOKIE, + args: [ + { name: 'prompt', positional: true, required: true, help: 'Text prompt describing the image' }, + { name: 'model', default: 'z-image-turbo', help: 'Model ID (z-image-turbo, flux-schnell, nano-banana, flux-2-pro, ...)' }, + { name: 'ratio', default: '1:1', choices: ['1:1', '16:9', '9:16', '4:3', '3:4'], help: 'Aspect ratio' }, + { name: 'image', help: 'Input image URL for image-to-image (upload via "opencli yollomi upload" first)' }, + { name: 'output', default: './yollomi-output', help: 'Output directory' }, + { name: 'no-download', type: 'boolean', default: false, help: 'Only show URLs, skip download' }, + ], + columns: ['index', 'status', 'file', 'size', 'url'], + func: async (page, kwargs) => { + const prompt = kwargs.prompt as string; + const modelId = kwargs.model as string; + const ratio = kwargs.ratio as string; + + const apiPath = MODEL_ROUTES[modelId]; + if (!apiPath) throw new CliError('INVALID_MODEL', `Unknown model: ${modelId}`, 'Run "opencli yollomi models --type image" to see available models'); + + let body: Record; + + if (modelId === 'z-image-turbo') { + const { width, height } = getDimensions(ratio); + body = { prompt, width, height, output_format: 'jpg', output_quality: 85, guidance_scale: 0, num_inference_steps: 8 }; + } else if (modelId === 'flux-2-pro') { + body = { prompt, aspectRatio: ratio, outputNumber: 1 }; + if (kwargs.image) body.imageUrl = kwargs.image as string; + } else if (modelId === 'flux-kontext-pro') { + body = { prompt, output_format: 'jpg' }; + if (kwargs.image) body.imageUrl = kwargs.image as string; + if (ratio !== '1:1') body.aspect_ratio = ratio; + } else { + body = { prompt, aspect_ratio: ratio }; + if (kwargs.image) body.imageUrl = kwargs.image as string; + } + + process.stderr.write(chalk.dim(`Generating with ${modelId}...\n`)); + const data = await yollomiPost(page, apiPath, body); + + const images: string[] = data.images || (data.image ? [data.image] : []); + if (!images.length) throw new CliError('EMPTY_RESPONSE', 'No images returned', 'Try a different prompt or model'); + + const noDownload = kwargs['no-download'] as boolean; + const outputDir = kwargs.output as string; + const results: any[] = []; + + for (let i = 0; i < images.length; i++) { + const url = images[i]; + if (noDownload) { + results.push({ index: i + 1, status: 'generated', file: '-', size: '-', url }); + continue; + } + try { + const ext = url.includes('.png') ? '.png' : '.jpg'; + const filename = `yollomi_${modelId}_${Date.now()}_${i + 1}${ext}`; + const { path: fp, size } = await downloadOutput(url, outputDir, filename); + results.push({ index: i + 1, status: 'saved', file: path.relative('.', fp), size: fmtBytes(size), url }); + } catch { + results.push({ index: i + 1, status: 'download-failed', file: '-', size: '-', url }); + } + } + + if (data.remainingCredits !== undefined) process.stderr.write(chalk.dim(`Credits remaining: ${data.remainingCredits}\n`)); + return results; + }, +}); diff --git a/src/clis/yollomi/models.ts b/src/clis/yollomi/models.ts new file mode 100644 index 0000000..3b00c19 --- /dev/null +++ b/src/clis/yollomi/models.ts @@ -0,0 +1,37 @@ +import { cli, Strategy } from '../../registry.js'; +import { IMAGE_MODELS, VIDEO_MODELS, TOOL_MODELS } from './utils.js'; + +cli({ + site: 'yollomi', + name: 'models', + description: 'List available Yollomi AI models (image, video, tools)', + strategy: Strategy.PUBLIC, + args: [ + { name: 'type', default: 'all', choices: ['all', 'image', 'video', 'tool'], help: 'Filter by model type' }, + ], + columns: ['type', 'model', 'credits', 'description'], + func: async (_page, kwargs) => { + const filter = kwargs.type as string; + const rows: { type: string; model: string; credits: number | string; description: string }[] = []; + + if (filter === 'all' || filter === 'image') { + for (const [id, info] of Object.entries(IMAGE_MODELS)) { + rows.push({ type: 'image', model: id, credits: info.credits, description: info.description }); + } + } + + if (filter === 'all' || filter === 'video') { + for (const [id, info] of Object.entries(VIDEO_MODELS)) { + rows.push({ type: 'video', model: id, credits: info.credits, description: info.description }); + } + } + + if (filter === 'all' || filter === 'tool') { + for (const [id, info] of Object.entries(TOOL_MODELS)) { + rows.push({ type: 'tool', model: id, credits: info.credits, description: info.description }); + } + } + + return rows; + }, +}); diff --git a/src/clis/yollomi/object-remover.ts b/src/clis/yollomi/object-remover.ts new file mode 100644 index 0000000..e2c8777 --- /dev/null +++ b/src/clis/yollomi/object-remover.ts @@ -0,0 +1,44 @@ +/** + * Yollomi object remover — POST /api/ai/object-remover + */ + +import * as path from 'node:path'; +import chalk from 'chalk'; +import { cli, Strategy } from '../../registry.js'; +import { CliError } from '../../errors.js'; +import { YOLLOMI_DOMAIN, yollomiPost, downloadOutput, fmtBytes } from './utils.js'; + +cli({ + site: 'yollomi', + name: 'object-remover', + description: 'Remove unwanted objects from images (3 credits)', + domain: YOLLOMI_DOMAIN, + strategy: Strategy.COOKIE, + args: [ + { name: 'image', required: true, help: 'Image URL' }, + { name: 'mask', required: true, help: 'Mask image URL (white = area to remove)' }, + { name: 'output', default: './yollomi-output', help: 'Output directory' }, + { name: 'no-download', type: 'boolean', default: false, help: 'Only show URL' }, + ], + columns: ['status', 'file', 'size', 'url'], + func: async (page, kwargs) => { + process.stderr.write(chalk.dim('Removing object...\n')); + const data = await yollomiPost(page, '/api/ai/object-remover', { + image: kwargs.image as string, + mask: kwargs.mask as string, + }); + + const url = data.image || (data.images?.[0]); + if (!url) throw new CliError('EMPTY_RESPONSE', 'No result', 'Check image and mask'); + + if (kwargs['no-download']) return [{ status: 'removed', file: '-', size: '-', url }]; + + try { + const filename = `yollomi_removed_${Date.now()}.png`; + const { path: fp, size } = await downloadOutput(url, kwargs.output as string, filename); + return [{ status: 'saved', file: path.relative('.', fp), size: fmtBytes(size), url }]; + } catch { + return [{ status: 'download-failed', file: '-', size: '-', url }]; + } + }, +}); diff --git a/src/clis/yollomi/remove-bg.ts b/src/clis/yollomi/remove-bg.ts new file mode 100644 index 0000000..2cfc3ce --- /dev/null +++ b/src/clis/yollomi/remove-bg.ts @@ -0,0 +1,40 @@ +/** + * Yollomi background removal — POST /api/ai/remove-bg (free, 0 credits) + */ + +import * as path from 'node:path'; +import chalk from 'chalk'; +import { cli, Strategy } from '../../registry.js'; +import { CliError } from '../../errors.js'; +import { YOLLOMI_DOMAIN, yollomiPost, downloadOutput, fmtBytes } from './utils.js'; + +cli({ + site: 'yollomi', + name: 'remove-bg', + description: 'Remove image background with AI (free)', + domain: YOLLOMI_DOMAIN, + strategy: Strategy.COOKIE, + args: [ + { name: 'image', positional: true, required: true, help: 'Image URL to remove background from' }, + { name: 'output', default: './yollomi-output', help: 'Output directory' }, + { name: 'no-download', type: 'boolean', default: false, help: 'Only show URL' }, + ], + columns: ['status', 'file', 'size', 'url'], + func: async (page, kwargs) => { + process.stderr.write(chalk.dim('Removing background...\n')); + const data = await yollomiPost(page, '/api/ai/remove-bg', { imageUrl: kwargs.image as string }); + + const url = data.image || (data.images?.[0]); + if (!url) throw new CliError('EMPTY_RESPONSE', 'No result', 'Check the input image URL'); + + if (kwargs['no-download']) return [{ status: 'processed', file: '-', size: '-', url }]; + + try { + const filename = `yollomi_nobg_${Date.now()}.png`; + const { path: fp, size } = await downloadOutput(url, kwargs.output as string, filename); + return [{ status: 'saved', file: path.relative('.', fp), size: fmtBytes(size), url }]; + } catch { + return [{ status: 'download-failed', file: '-', size: '-', url }]; + } + }, +}); diff --git a/src/clis/yollomi/restore.ts b/src/clis/yollomi/restore.ts new file mode 100644 index 0000000..7c3c883 --- /dev/null +++ b/src/clis/yollomi/restore.ts @@ -0,0 +1,40 @@ +/** + * Yollomi photo restoration — POST /api/ai/photo-restoration + */ + +import * as path from 'node:path'; +import chalk from 'chalk'; +import { cli, Strategy } from '../../registry.js'; +import { CliError } from '../../errors.js'; +import { YOLLOMI_DOMAIN, yollomiPost, downloadOutput, fmtBytes } from './utils.js'; + +cli({ + site: 'yollomi', + name: 'restore', + description: 'Restore old or damaged photos with AI (4 credits)', + domain: YOLLOMI_DOMAIN, + strategy: Strategy.COOKIE, + args: [ + { name: 'image', positional: true, required: true, help: 'Image URL to restore' }, + { name: 'output', default: './yollomi-output', help: 'Output directory' }, + { name: 'no-download', type: 'boolean', default: false, help: 'Only show URL' }, + ], + columns: ['status', 'file', 'size', 'url'], + func: async (page, kwargs) => { + process.stderr.write(chalk.dim('Restoring photo...\n')); + const data = await yollomiPost(page, '/api/ai/photo-restoration', { imageUrl: kwargs.image as string }); + + const url = data.image || (data.images?.[0]); + if (!url) throw new CliError('EMPTY_RESPONSE', 'No result', 'Check the input image'); + + if (kwargs['no-download']) return [{ status: 'restored', file: '-', size: '-', url }]; + + try { + const filename = `yollomi_restored_${Date.now()}.jpg`; + const { path: fp, size } = await downloadOutput(url, kwargs.output as string, filename); + return [{ status: 'saved', file: path.relative('.', fp), size: fmtBytes(size), url }]; + } catch { + return [{ status: 'download-failed', file: '-', size: '-', url }]; + } + }, +}); diff --git a/src/clis/yollomi/try-on.ts b/src/clis/yollomi/try-on.ts new file mode 100644 index 0000000..b873796 --- /dev/null +++ b/src/clis/yollomi/try-on.ts @@ -0,0 +1,48 @@ +/** + * Yollomi virtual try-on — POST /api/ai/virtual-try-on + */ + +import * as path from 'node:path'; +import chalk from 'chalk'; +import { cli, Strategy } from '../../registry.js'; +import { CliError } from '../../errors.js'; +import { YOLLOMI_DOMAIN, yollomiPost, downloadOutput, fmtBytes } from './utils.js'; + +cli({ + site: 'yollomi', + name: 'try-on', + description: 'Virtual try-on — see how clothes look on a person (3 credits)', + domain: YOLLOMI_DOMAIN, + strategy: Strategy.COOKIE, + args: [ + { name: 'person', required: true, help: 'Person photo URL (upload via "opencli yollomi upload" first)' }, + { name: 'cloth', required: true, help: 'Clothing image URL' }, + { name: 'cloth-type', default: 'upper', choices: ['upper', 'lower', 'overall'], help: 'Clothing type' }, + { name: 'output', default: './yollomi-output', help: 'Output directory' }, + { name: 'no-download', type: 'boolean', default: false, help: 'Only show URL' }, + ], + columns: ['status', 'file', 'size', 'url'], + func: async (page, kwargs) => { + process.stderr.write(chalk.dim('Processing virtual try-on...\n')); + const data = await yollomiPost(page, '/api/ai/virtual-try-on', { + person_image: kwargs.person as string, + cloth_image: kwargs.cloth as string, + cloth_type: kwargs['cloth-type'] as string, + output_format: 'png', + output_quality: 100, + }); + + const url = data.image || (data.images?.[0]); + if (!url) throw new CliError('EMPTY_RESPONSE', 'No result', 'Check both images have clear subjects'); + + if (kwargs['no-download']) return [{ status: 'generated', file: '-', size: '-', url }]; + + try { + const filename = `yollomi_tryon_${Date.now()}.png`; + const { path: fp, size } = await downloadOutput(url, kwargs.output as string, filename); + return [{ status: 'saved', file: path.relative('.', fp), size: fmtBytes(size), url }]; + } catch { + return [{ status: 'download-failed', file: '-', size: '-', url }]; + } + }, +}); diff --git a/src/clis/yollomi/upload.ts b/src/clis/yollomi/upload.ts new file mode 100644 index 0000000..7ae047e --- /dev/null +++ b/src/clis/yollomi/upload.ts @@ -0,0 +1,76 @@ +/** + * Yollomi image upload — POST /api/upload (FormData) + * + * Uploads a local file to Yollomi's R2 storage and returns the URL. + * The URL can then be used as input for image-to-image, face-swap, etc. + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import chalk from 'chalk'; +import { cli, Strategy } from '../../registry.js'; +import { CliError } from '../../errors.js'; +import { YOLLOMI_DOMAIN, ensureOnYollomi, fmtBytes } from './utils.js'; + +const MIME_MAP: Record = { + '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', + '.png': 'image/png', '.gif': 'image/gif', + '.webp': 'image/webp', + '.mp4': 'video/mp4', '.mov': 'video/quicktime', +}; + +cli({ + site: 'yollomi', + name: 'upload', + description: 'Upload an image or video to Yollomi (returns URL for other commands)', + domain: YOLLOMI_DOMAIN, + strategy: Strategy.COOKIE, + args: [ + { name: 'file', positional: true, required: true, help: 'Local file path to upload' }, + ], + columns: ['status', 'file', 'size', 'url'], + func: async (page, kwargs) => { + const filePath = path.resolve(kwargs.file as string); + if (!fs.existsSync(filePath)) throw new CliError('FILE_NOT_FOUND', `File not found: ${filePath}`, 'Provide a valid file path'); + + const ext = path.extname(filePath).toLowerCase(); + const mime = MIME_MAP[ext]; + if (!mime) throw new CliError('INVALID_TYPE', `Unsupported file type: ${ext}`, 'Supported: jpg, png, gif, webp, mp4, mov'); + + const data = fs.readFileSync(filePath); + const maxSize = mime.startsWith('video/') ? 100 * 1024 * 1024 : 10 * 1024 * 1024; + if (data.length > maxSize) throw new CliError('FILE_TOO_LARGE', `File too large: ${fmtBytes(data.length)}`, `Max ${mime.startsWith('video/') ? '100MB' : '10MB'}`); + + const b64 = data.toString('base64'); + const fileName = path.basename(filePath); + + process.stderr.write(chalk.dim(`Uploading ${fileName} (${fmtBytes(data.length)})...\n`)); + await ensureOnYollomi(page); + + const result = await page.evaluate(` + (async () => { + try { + const raw = atob(${JSON.stringify(b64)}); + const arr = new Uint8Array(raw.length); + for (let i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i); + const file = new File([arr], ${JSON.stringify(fileName)}, { type: ${JSON.stringify(mime)} }); + const fd = new FormData(); + fd.append('file', file); + const res = await fetch('/api/upload', { method: 'POST', body: fd, credentials: 'include' }); + const json = await res.json(); + return { ok: res.ok, status: res.status, data: json }; + } catch (err) { + return { ok: false, status: 0, data: { error: err.message } }; + } + })() + `); + + if (!result?.ok) { + throw new CliError('UPLOAD_ERROR', result?.data?.error || 'Upload failed', 'Make sure you are logged in to yollomi.com'); + } + + const url = result.data.url; + process.stderr.write(chalk.green(`Uploaded! Use this URL as input for other commands.\n`)); + return [{ status: 'uploaded', file: fileName, size: fmtBytes(data.length), url }]; + }, +}); diff --git a/src/clis/yollomi/upscale.ts b/src/clis/yollomi/upscale.ts new file mode 100644 index 0000000..120c0d6 --- /dev/null +++ b/src/clis/yollomi/upscale.ts @@ -0,0 +1,48 @@ +/** + * Yollomi image upscaling — POST /api/ai/image-upscaler + */ + +import * as path from 'node:path'; +import chalk from 'chalk'; +import { cli, Strategy } from '../../registry.js'; +import { CliError } from '../../errors.js'; +import { YOLLOMI_DOMAIN, yollomiPost, downloadOutput, fmtBytes } from './utils.js'; + +cli({ + site: 'yollomi', + name: 'upscale', + description: 'Upscale image resolution with AI (1 credit)', + domain: YOLLOMI_DOMAIN, + strategy: Strategy.COOKIE, + args: [ + { name: 'image', positional: true, required: true, help: 'Image URL to upscale' }, + { name: 'scale', type: 'int', default: 2, help: 'Upscale factor (2 or 4)' }, + { name: 'output', default: './yollomi-output', help: 'Output directory' }, + { name: 'no-download', type: 'boolean', default: false, help: 'Only show URL' }, + ], + columns: ['status', 'file', 'size', 'scale', 'url'], + func: async (page, kwargs) => { + const scale = kwargs.scale as number; + process.stderr.write(chalk.dim(`Upscaling ${scale}x...\n`)); + const data = await yollomiPost(page, '/api/ai/image-upscaler', { + imageUrl: kwargs.image as string, + scale, + face_enhance: false, + }); + + const url = data.image || (data.images?.[0]); + if (!url) throw new CliError('EMPTY_RESPONSE', 'No result', 'Check the input image'); + + if (kwargs['no-download']) return [{ status: 'upscaled', file: '-', size: '-', scale: `${scale}x`, url }]; + + try { + const ext = url.includes('.png') ? '.png' : '.jpg'; + const filename = `yollomi_upscale_${scale}x_${Date.now()}${ext}`; + const { path: fp, size } = await downloadOutput(url, kwargs.output as string, filename); + if (data.remainingCredits !== undefined) process.stderr.write(chalk.dim(`Credits remaining: ${data.remainingCredits}\n`)); + return [{ status: 'saved', file: path.relative('.', fp), size: fmtBytes(size), scale: `${scale}x`, url }]; + } catch { + return [{ status: 'download-failed', file: '-', size: '-', scale: `${scale}x`, url }]; + } + }, +}); diff --git a/src/clis/yollomi/utils.ts b/src/clis/yollomi/utils.ts new file mode 100644 index 0000000..d9961d6 --- /dev/null +++ b/src/clis/yollomi/utils.ts @@ -0,0 +1,202 @@ +/** + * Yollomi API utilities — browser cookie strategy. + * + * Uses the same per-model API routes as the Yollomi frontend: + * POST /api/ai/ — image generation (session cookie auth) + * POST /api/ai/video — video generation (session cookie auth) + * + * Auth: browser session cookies from NextAuth — just log in to yollomi.com in Chrome. + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import type { IPage } from '../../types.js'; +import { CliError } from '../../errors.js'; + +export const YOLLOMI_DOMAIN = 'yollomi.com'; + +/** + * Ensure the browser tab is on yollomi.com. + * The framework pre-nav sometimes silently fails, leaving the page on about:blank. + */ +export async function ensureOnYollomi(page: IPage): Promise { + const currentUrl = await page.evaluate(`(() => location.href)()`) as string; + if (!currentUrl || !currentUrl.includes('yollomi.com')) { + await page.goto('https://yollomi.com'); + await page.wait(3); + } +} + +/** + * POST to a Yollomi /api/ai/* route via the browser session. + * Uses relative paths (e.g. `/api/ai/flux`) — same as the frontend. + */ +export async function yollomiPost(page: IPage, apiPath: string, body: Record): Promise { + const bodyJson = JSON.stringify(body); + await ensureOnYollomi(page); + + const result = await page.evaluate(` + (async () => { + try { + const res = await fetch(${JSON.stringify(apiPath)}, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: ${JSON.stringify(bodyJson)}, + }); + const text = await res.text(); + return { ok: res.ok, status: res.status, body: text }; + } catch (err) { + return { ok: false, status: 0, body: err.message || 'fetch failed (on ' + location.href + ')' }; + } + })() + `); + + if (!result || result.status === 0) { + throw new CliError( + 'FETCH_ERROR', + `Network error: ${result?.body || 'Failed to fetch'}`, + 'Make sure Chrome is logged in to https://yollomi.com and the Browser Bridge is running', + ); + } + + if (!result.ok) { + let detail = result.body; + try { detail = JSON.parse(result.body)?.error || JSON.parse(result.body)?.message || result.body; } catch {} + throw new CliError( + 'API_ERROR', + `Yollomi API ${result.status}: ${detail}`, + result.status === 401 + ? 'Not logged in — open Chrome, go to https://yollomi.com and log in' + : result.status === 402 + ? 'Insufficient credits — top up at https://yollomi.com/pricing' + : result.status === 429 + ? 'Rate limited — wait a moment and retry' + : 'Check the model and parameters', + ); + } + + try { + return JSON.parse(result.body); + } catch { + throw new CliError('API_ERROR', 'Invalid JSON response', 'Try again'); + } +} + +/** + * Resolve an image input: local file → base64 data URL, URL → as-is. + */ +export function resolveImageInput(input: string): string { + if (input.startsWith('http://') || input.startsWith('https://') || input.startsWith('data:')) { + return input; + } + + const resolved = path.resolve(input); + if (!fs.existsSync(resolved)) { + throw new CliError('FILE_NOT_FOUND', `File not found: ${resolved}`, 'Provide a valid file path or URL'); + } + + const ext = path.extname(resolved).toLowerCase(); + const mimeMap: Record = { + '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', + '.png': 'image/png', '.gif': 'image/gif', + '.webp': 'image/webp', '.bmp': 'image/bmp', + }; + const mime = mimeMap[ext] || 'image/png'; + const data = fs.readFileSync(resolved); + return `data:${mime};base64,${data.toString('base64')}`; +} + +export async function downloadOutput( + url: string, outputDir: string, filename: string, +): Promise<{ path: string; size: number }> { + fs.mkdirSync(outputDir, { recursive: true }); + const destPath = path.join(outputDir, filename); + const resp = await fetch(url); + if (!resp.ok) throw new CliError('DOWNLOAD_ERROR', `Download failed: HTTP ${resp.status}`, 'URL may have expired'); + const buffer = Buffer.from(await resp.arrayBuffer()); + fs.writeFileSync(destPath, buffer); + return { path: destPath, size: buffer.length }; +} + +export function fmtBytes(bytes: number): string { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`; +} + +/** Per-model API route mapping (matches frontend model.apiEndpoint). */ +export const MODEL_ROUTES: Record = { + 'flux': '/api/ai/flux', + 'flux-schnell': '/api/ai/flux-schnell', + 'flux-2-pro': '/api/ai/flux-2-pro', + 'flux-kontext-pro': '/api/ai/flux-kontext-pro', + 'nano-banana': '/api/ai/nano-banana', + 'nano-banana-pro': '/api/ai/nano-banana-pro', + 'nano-banana-2': '/api/ai/nano-banana-2', + 'z-image-turbo': '/api/ai/z-image-turbo', + 'imagen-4-ultra': '/api/ai/imagen-4-ultra', + 'imagen-4-fast': '/api/ai/imagen-4-fast', + 'ideogram-v3-turbo': '/api/ai/ideogram-v3-turbo', + 'stable-diffusion-3-5-large':'/api/ai/stable-diffusion-3-5-large', + 'seedream-4-5': '/api/ai/seedream-4-5', + 'seedream-5-lite': '/api/ai/seedream-5-lite', + 'qwen-image-edit': '/api/ai/qwen-image-edit', + 'qwen-image-edit-plus': '/api/ai/qwen-image-edit-plus', + 'remove-bg': '/api/ai/remove-bg', + 'image-upscaler': '/api/ai/image-upscaler', + 'face-swap': '/api/ai/face-swap', + 'virtual-try-on': '/api/ai/virtual-try-on', + 'photo-restoration': '/api/ai/photo-restoration', + 'ai-background-generator': '/api/ai/ai-background-generator', + 'object-remover': '/api/ai/object-remover', +}; + +/** Well-known image model IDs and their credit costs. */ +export const IMAGE_MODELS: Record = { + 'z-image-turbo': { credits: 1, description: 'Alibaba Qwen turbo (cheapest, 1 credit)' }, + 'flux-schnell': { credits: 2, description: 'High-speed Flux generation' }, + 'ideogram-v3-turbo': { credits: 3, description: 'Ideogram V3 Turbo' }, + 'imagen-4-fast': { credits: 3, description: 'Google Imagen 4 Fast' }, + 'seedream-4-5': { credits: 4, description: 'Seedream 4.5 (ByteDance)' }, + 'seedream-5-lite': { credits: 4, description: 'Seedream 5 Lite — 2K/3K' }, + 'flux': { credits: 4, description: 'Flux 1.1 Pro' }, + 'nano-banana': { credits: 4, description: 'Google Nano Banana' }, + 'flux-kontext-pro': { credits: 4, description: 'Flux Kontext Pro (img2img)' }, + 'imagen-4-ultra': { credits: 6, description: 'Google Imagen 4 Ultra' }, + 'nano-banana-2': { credits: 7, description: 'Google Nano Banana 2' }, + 'stable-diffusion-3-5-large':{ credits: 7, description: 'Stable Diffusion 3.5 Large' }, + 'nano-banana-pro': { credits: 15, description: 'Nano Banana Pro' }, + 'flux-2-pro': { credits: 15, description: 'Flux 2 Pro (premium)' }, +}; + +export const VIDEO_MODELS: Record = { + 'kling-v2-6-motion-control': { credits: 7, description: 'Kling v2.6 Motion Control' }, + 'bytedance-seedance-1-pro-fast': { credits: 8, description: 'Seedance 1.0 Pro Fast' }, + 'kling-2-1': { credits: 9, description: 'Kling 2.1' }, + 'minimax-hailuo-2-3': { credits: 9, description: 'Hailuo 2.3' }, + 'pixverse-5': { credits: 9, description: 'PixVerse 5' }, + 'wan-2-5-t2v': { credits: 9, description: 'Wan 2.5 Text-to-Video' }, + 'wan-2-5-i2v': { credits: 9, description: 'Wan 2.5 Image-to-Video' }, + 'google-veo-3-fast': { credits: 9, description: 'Google Veo 3 Fast' }, + 'google-veo-3-1-fast': { credits: 9, description: 'Google Veo 3.1 Fast' }, + 'openai-sora-2': { credits: 10, description: 'Sora 2' }, + 'google-veo-3': { credits: 10, description: 'Google Veo 3' }, + 'google-veo-3-1': { credits: 10, description: 'Google Veo 3.1' }, + 'wan-2-6-t2v': { credits: 29, description: 'Wan 2.6 T2V (premium)' }, + 'wan-2-6-i2v': { credits: 29, description: 'Wan 2.6 I2V (premium)' }, +}; + +export const TOOL_MODELS: Record = { + 'remove-bg': { credits: 0, description: 'Remove background (free)' }, + 'image-upscaler': { credits: 1, description: 'Enhance image resolution' }, + 'object-remover': { credits: 3, description: 'Remove unwanted objects' }, + 'face-swap': { credits: 3, description: 'Swap faces in photos' }, + 'virtual-try-on': { credits: 3, description: 'Try clothes on photos' }, + 'qwen-image-edit': { credits: 3, description: 'Edit image with text prompt' }, + 'qwen-image-edit-plus': { credits: 3, description: 'Advanced image editing' }, + 'photo-restoration': { credits: 4, description: 'Revive old/damaged photos' }, + 'ai-background-generator':{ credits: 5, description: 'Generate custom backgrounds' }, +}; diff --git a/src/clis/yollomi/video.ts b/src/clis/yollomi/video.ts new file mode 100644 index 0000000..7d2d71b --- /dev/null +++ b/src/clis/yollomi/video.ts @@ -0,0 +1,61 @@ +/** + * Yollomi video generation — POST /api/ai/video + * Matches the frontend video-generator.tsx request format exactly. + */ + +import * as path from 'node:path'; +import chalk from 'chalk'; +import { cli, Strategy } from '../../registry.js'; +import { CliError } from '../../errors.js'; +import { YOLLOMI_DOMAIN, yollomiPost, downloadOutput, fmtBytes } from './utils.js'; + +cli({ + site: 'yollomi', + name: 'video', + description: 'Generate videos with AI (text-to-video or image-to-video)', + domain: YOLLOMI_DOMAIN, + strategy: Strategy.COOKIE, + args: [ + { name: 'prompt', positional: true, required: true, help: 'Text prompt describing the video' }, + { name: 'model', default: 'kling-2-1', help: 'Model (kling-2-1, openai-sora-2, google-veo-3-1, wan-2-5-t2v, ...)' }, + { name: 'image', help: 'Input image URL for image-to-video' }, + { name: 'ratio', default: '16:9', choices: ['1:1', '16:9', '9:16', '4:3', '3:4'], help: 'Aspect ratio' }, + { name: 'output', default: './yollomi-output', help: 'Output directory' }, + { name: 'no-download', type: 'boolean', default: false, help: 'Only show URL, skip download' }, + ], + columns: ['status', 'file', 'size', 'credits', 'url'], + func: async (page, kwargs) => { + const prompt = kwargs.prompt as string; + const modelId = kwargs.model as string; + + const inputs: Record = { + aspect_ratio: kwargs.ratio as string, + }; + if (kwargs.image) inputs.image = kwargs.image as string; + + const body = { modelId, prompt, inputs }; + + process.stderr.write(chalk.dim(`Generating video with ${modelId} (may take a while)...\n`)); + const data = await yollomiPost(page, '/api/ai/video', body); + + const videoUrl: string = data.video || ''; + if (!videoUrl) throw new CliError('EMPTY_RESPONSE', 'No video returned', 'Try a different prompt or model'); + + const credits = data.remainingCredits; + const noDownload = kwargs['no-download'] as boolean; + const outputDir = kwargs.output as string; + + if (noDownload) { + return [{ status: 'generated', file: '-', size: '-', credits: credits ?? '-', url: videoUrl }]; + } + + try { + const filename = `yollomi_${modelId}_${Date.now()}.mp4`; + const { path: fp, size } = await downloadOutput(videoUrl, outputDir, filename); + if (credits !== undefined) process.stderr.write(chalk.dim(`Credits remaining: ${credits}\n`)); + return [{ status: 'saved', file: path.relative('.', fp), size: fmtBytes(size), credits: credits ?? '-', url: videoUrl }]; + } catch { + return [{ status: 'download-failed', file: '-', size: '-', credits: credits ?? '-', url: videoUrl }]; + } + }, +}); From 69989f80b46ea69012fb0578d5cf71d446678664 Mon Sep 17 00:00:00 2001 From: anichikage Date: Sun, 22 Mar 2026 16:45:54 +0800 Subject: [PATCH 2/2] 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. --- docs/.vitepress/config.mts | 1 + docs/adapters/browser/yollomi.md | 69 ++++++++++++++++++++++++++++++++ docs/adapters/index.md | 1 + 3 files changed, 71 insertions(+) create mode 100644 docs/adapters/browser/yollomi.md diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 533c3c2..3cc0f51 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -64,6 +64,7 @@ export default defineConfig({ { text: 'SMZDM', link: '/adapters/browser/smzdm' }, { text: 'Jike', link: '/adapters/browser/jike' }, { text: 'Jimeng', link: '/adapters/browser/jimeng' }, + { text: 'Yollomi', link: '/adapters/browser/yollomi' }, { text: 'LINUX DO', link: '/adapters/browser/linux-do' }, { text: 'Chaoxing', link: '/adapters/browser/chaoxing' }, { text: 'Grok', link: '/adapters/browser/grok' }, diff --git a/docs/adapters/browser/yollomi.md b/docs/adapters/browser/yollomi.md new file mode 100644 index 0000000..e38f7ed --- /dev/null +++ b/docs/adapters/browser/yollomi.md @@ -0,0 +1,69 @@ +# Yollomi + +**Mode**: 🔐 Browser · **Domain**: `yollomi.com` + +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). + +## Commands + +| Command | Description | +|---------|-------------| +| `opencli yollomi generate` | Text-to-image / image-to-image | +| `opencli yollomi video` | Text-to-video / image-to-video | +| `opencli yollomi edit` | Qwen image edit (prompt + image) | +| `opencli yollomi upload` | Upload a local file → public URL for other commands | +| `opencli yollomi models` | List image / video / tool models and credit costs | +| `opencli yollomi remove-bg` | Remove background (free) | +| `opencli yollomi upscale` | Image upscaling | +| `opencli yollomi face-swap` | Face swap between two images | +| `opencli yollomi restore` | Photo restoration | +| `opencli yollomi try-on` | Virtual try-on | +| `opencli yollomi background` | AI background for product/object images | +| `opencli yollomi object-remover` | Remove objects (image + mask URLs) | + +## Usage Examples + +```bash +# List models +opencli yollomi models --type image + +# Text-to-image (default model: z-image-turbo) +opencli yollomi generate "a red apple on a wooden table" + +# Choose model and aspect ratio +opencli yollomi generate "sunset" --model flux-schnell --ratio 16:9 + +# Image-to-image: upload first, then pass URL +opencli yollomi upload ./photo.png +opencli yollomi generate "oil painting style" --model flux-2-pro --image "https://..." + +# Video +opencli yollomi video "waves on a beach" --model kling-2-1 + +# Tools +opencli yollomi remove-bg https://example.com/image.png +opencli yollomi upscale https://example.com/image.png --scale 4 +opencli yollomi edit https://example.com/in.png "make it vintage" +``` + +### Common options + +| Option | Applies to | Description | +|--------|------------|-------------| +| `--model` | `generate`, `video` | Model id (see `yollomi models`) | +| `--ratio` | `generate`, `video` | Aspect ratio, e.g. `1:1`, `16:9` | +| `--image` | `generate`, `video` | Image URL for img2img / i2v | +| `--output` | Most | Output directory (default `./yollomi-output`) | +| `--no-download` | Several | Print URLs only, skip saving files | + +## Prerequisites + +- Chrome running and **logged into** [yollomi.com](https://yollomi.com) (Google OAuth) +- [Browser Bridge extension](/guide/browser-bridge) installed; daemon connects on first command + +The CLI ensures the automation tab is on `yollomi.com` before calling APIs (same-origin `fetch` with session cookies). + +## Notes + +- **Credits**: Each model consumes account credits; insufficient credits returns HTTP 402. +- **Upload**: Local paths for tools are not accepted directly — use `yollomi upload` to get a URL, or pass an existing HTTPS image URL. diff --git a/docs/adapters/index.md b/docs/adapters/index.md index 2944f84..53a929b 100644 --- a/docs/adapters/index.md +++ b/docs/adapters/index.md @@ -24,6 +24,7 @@ Run `opencli list` for the live registry. | **[smzdm](/adapters/browser/smzdm)** | `search` | 🔐 Browser | | **[jike](/adapters/browser/jike)** | `feed` `search` `post` `topic` `user` `create` `comment` `like` `repost` `notifications` | 🔐 Browser | | **[jimeng](/adapters/browser/jimeng)** | `generate` `history` | 🔐 Browser | +| **[yollomi](/adapters/browser/yollomi)** | `generate` `video` `edit` `upload` `models` `remove-bg` `upscale` `face-swap` `restore` `try-on` `background` `object-remover` | 🔐 Browser | | **[linux-do](/adapters/browser/linux-do)** | `hot` `latest` `categories` `category` `search` `topic` | 🔐 Browser | | **[chaoxing](/adapters/browser/chaoxing)** | `assignments` `exams` | 🔐 Browser | | **[grok](/adapters/browser/grok)** | `ask` | 🔐 Browser |