museav-cli 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +17 -2
- package/CHANGELOG.md +31 -0
- package/README.md +52 -4
- package/dist/client.d.ts +51 -0
- package/dist/client.js +39 -0
- package/dist/commands/gen.d.ts +1 -0
- package/dist/commands/gen.js +12 -0
- package/dist/commands/img-tools.d.ts +16 -0
- package/dist/commands/img-tools.js +81 -0
- package/dist/commands/jobs.d.ts +1 -0
- package/dist/commands/jobs.js +7 -1
- package/dist/commands/projects.d.ts +21 -0
- package/dist/commands/projects.js +69 -0
- package/dist/commands/reverse.d.ts +7 -2
- package/dist/commands/reverse.js +28 -2
- package/dist/commands/templates.js +2 -1
- package/dist/commands/video-templates.d.ts +2 -0
- package/dist/commands/video-templates.js +14 -5
- package/dist/index.js +85 -7
- package/dist/local-bg.d.ts +17 -0
- package/dist/local-bg.js +143 -0
- package/dist/local-vision.d.ts +14 -0
- package/dist/local-vision.js +134 -0
- package/package.json +3 -2
- package/src/client.ts +94 -13
- package/src/commands/gen.ts +14 -0
- package/src/commands/img-tools.ts +95 -0
- package/src/commands/jobs.ts +8 -2
- package/src/commands/projects.ts +80 -0
- package/src/commands/reverse.ts +36 -4
- package/src/commands/templates.ts +2 -1
- package/src/commands/video-templates.ts +15 -4
- package/src/index.ts +93 -7
- package/src/local-bg.ts +142 -0
- package/src/local-vision.ts +150 -0
package/AGENTS.md
CHANGED
|
@@ -19,8 +19,10 @@ Don't try both — whichever credential is present is what gets used (env var `S
|
|
|
19
19
|
# Generate an image, wait for it, get the URL on stdout
|
|
20
20
|
museav gen --prompt 'a poster, neon lights, cyberpunk' --ratio 9:16
|
|
21
21
|
|
|
22
|
-
# Generate a video (文生视频/图生视频), wait, get the mp4 URL on stdout
|
|
23
|
-
|
|
22
|
+
# Generate a video (文生视频/图生视频), wait, get the mp4 URL on stdout.
|
|
23
|
+
# Omit --model to let the platform route automatically (auto); pinning a tier uses
|
|
24
|
+
# artsdance-* ids like artsdance-2-0-pro-260801 (Seedance 2.0).
|
|
25
|
+
museav gen --video --prompt 'a cat stretching on a windowsill, cinematic' --ratio 9:16
|
|
24
26
|
museav gen --video --image logo.png --prompt 'logo glows slowly, background fades' --ratio 1:1
|
|
25
27
|
|
|
26
28
|
# Generate from a pre-configured image template instead of a raw prompt (deterministic
|
|
@@ -37,6 +39,10 @@ museav gen --template <id> --fields '{"artist":"name","city":"place"}'
|
|
|
37
39
|
museav templates create --name '演唱会海报' --prompt '{artist} 在 {city} 的演唱会海报' --ratio 9:16
|
|
38
40
|
|
|
39
41
|
# Reverse-engineer a prompt from an existing image (stdout: English prompt only).
|
|
42
|
+
# PRIMARY path is LOCAL: Ollama qwen3-vl — fast, free, NO login needed. Falls back to
|
|
43
|
+
# the platform API (with a slowness warning) only if Ollama isn't running or the model
|
|
44
|
+
# is missing. Image URLs always go to the API (local path takes file paths only);
|
|
45
|
+
# --api forces the API path.
|
|
40
46
|
# This READS the image and nothing else — it will NOT build a template. Passing any
|
|
41
47
|
# template-ish flag to the underlying API is a hard 400 since 2026-08-16.
|
|
42
48
|
museav reverse ./photo.png
|
|
@@ -53,6 +59,14 @@ museav image-to-template ./poster.jpg --no-create # dry run: draft JSON on
|
|
|
53
59
|
# Upload a file (image/audio/video; type is detected from the bytes, not the extension)
|
|
54
60
|
museav upload ./face.png
|
|
55
61
|
|
|
62
|
+
# Local image tools — no login, no platform quota, work on macOS AND Windows
|
|
63
|
+
# (all deps ship prebuilt binaries; zero platform-specific code):
|
|
64
|
+
# compress: resize/re-encode via sharp. Output <name>-min.<fmt>, never overwrites input.
|
|
65
|
+
# remove-bg: ISNet via onnxruntime-node → alpha PNG (~170MB model auto-downloaded
|
|
66
|
+
# to ~/.museav-models on first use; %USERPROFILE%\.museav-models on Windows).
|
|
67
|
+
museav compress ./photo.jpg --max-edge 800 --format webp --quality 70
|
|
68
|
+
museav remove-bg ./shoe.png # stdout: path to <name>-nobg.png
|
|
69
|
+
|
|
56
70
|
# List your own (or, if using a tenant apiKey, your tenant's) recent jobs as JSON
|
|
57
71
|
museav jobs --limit 10 --status failed
|
|
58
72
|
|
|
@@ -75,6 +89,7 @@ Full flag reference: `museav <command> --help`. Full command table and auth deta
|
|
|
75
89
|
|
|
76
90
|
## Failure modes worth knowing
|
|
77
91
|
|
|
92
|
+
- **Cross-platform contract**: the CLI targets macOS AND Windows. No Unix-only assumptions anywhere — paths go through `node:path`/`os.homedir()`, no shell expansions, no brew/which calls in code (OS-specific text like Ollama start hints adapts via `process.platform`). Keep it that way in new code.
|
|
78
93
|
- `gen` polls until the job finishes or times out (default 600s controlled by the underlying `generateAndWait`); a timeout throws, it does not hang forever.
|
|
79
94
|
- `jobs --limit`/`--status` are filtered **client-side** — the server always returns your most recent 50 jobs; you cannot page past that.
|
|
80
95
|
- Non-zero exit code + a message on stderr is the only failure signal; there's no separate machine-readable error format on stdout.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.3.0 · 2026-08-17
|
|
4
|
+
|
|
5
|
+
**项目(工作区)与项目素材库。** 层级:平台 → 账户 → 工作区,素材挂工作区——人像库的项目出模特图、产品库的项目出电商图,业务隔离互不污染。`--project` 收 id 或名称。
|
|
6
|
+
|
|
7
|
+
- `projects` 列表 / `projects create --name` 新建(每账户最多 5 个)
|
|
8
|
+
- `projects assets --project` 素材库 ls / add / rm:素材按**母版不压缩**上传,类型按字节判定(图片/音频/视频),stdout 出 `id<TAB>url` 可直接喂 `--ref`
|
|
9
|
+
- `gen --project` 出图/出视频归档进项目;`jobs --project` 按项目查任务
|
|
10
|
+
- `--ref` / `--image` 现在也收 http(s) 直链(素材库 URL 直接垫图,不再只认本地文件)
|
|
11
|
+
- 需中台 `workspace-assets` 接口(2026-08-17 已上线);账户 Key 白名单已放行
|
|
12
|
+
|
|
13
|
+
**本地图像工具箱(免登录、零成本、macOS/Windows 通用)。** 依赖全走 npm 预编译,代码零平台假设。
|
|
14
|
+
|
|
15
|
+
- `compress <file>`:sharp 压缩,`--max-edge / --quality / --format`,默认输出 `<名>-min.<格式>` 不覆写原文件,实测 597KB → 18KB
|
|
16
|
+
- `remove-bg <file>`:本地抠图去背景(ISNet + onnxruntime,刻意没用 AGPL 的现成包),输出带 alpha 的 PNG,热跑秒级;模型首次自动下载 ~170MB 缓存 `~/.museav-models`
|
|
17
|
+
- Ollama 未运行时的启动指引按操作系统区分(Windows 不再提示 brew)
|
|
18
|
+
|
|
19
|
+
## 2.2.0 · 2026-08-17
|
|
20
|
+
|
|
21
|
+
**`reverse` 主路改本地:Ollama + qwen3-vl。** 中台 API 读图要上传、排队、等云端推理;本地 8b 量化模型在自己机器上跑,免登录、零成本、离线可用。本地不可用(服务没起 / 模型没拉 / 推理出错)自动回落中台 API,回落时明确提示较慢,缺什么会告诉你补什么命令。
|
|
22
|
+
|
|
23
|
+
- 一次性配置:`brew install ollama && brew services start ollama && ollama pull qwen3-vl:8b`(Intel Mac / Linux 用官方安装脚本同理)。换模型档位设 `MUSEAV_LOCAL_VLM`(如 `qwen3-vl:4b`,更快、识图质量略降),自定服务地址走标准 `OLLAMA_HOST`。
|
|
24
|
+
- SCULPT 提示词从中台移植,砍掉本地不消费的 genre / body_md;返回归一化后与 API 路完全同构,`$(museav reverse x.png)` 管线用法不变,stdout 仍只出英文 prompt。
|
|
25
|
+
- 读图前复用上传同款压缩(压到视觉模型够用的尺寸),本地推理也跟着快。
|
|
26
|
+
- URL 输入固定走中台 API(本地路只收文件路径);`--api` 强制走中台。
|
|
27
|
+
- 本地路完全不需要中台凭证——未登录也能用,只有真回落 API 时才提示登录。
|
|
28
|
+
|
|
29
|
+
**修复 `video-templates create` 必被 400 拒**:中台视频模板契约硬性要求 `slug`(required=['zh_name','slug']),且 toapis 2026-08-14 下线后 CLI 默认的 `seedance-2` 成了悬空引用——不带参数建视频模板必挂。现在 `--slug` 缺省自动生成 `vt-` 短标识并在 stderr 回显,默认模型改 `auto`(交给中台路由),`--duration` 放宽到 4-30(Seedance 2.5 支持到 30 秒)。
|
|
30
|
+
|
|
31
|
+
- `templates` / `video-templates` 列表兼容读 config 顶层的 `fields`(现行契约存法——CLI 自己 create 写的就是顶层,之前自己建的模板自己都列不出字段)。
|
|
32
|
+
- `gen` / `video-templates` 帮助文案与 README / AGENTS.md 示例里已下线的 `seedance-2-*` 代号全部清掉,照旧文案传参会 400。
|
|
33
|
+
|
|
3
34
|
## 2.0.0 · 2026-08-16
|
|
4
35
|
|
|
5
36
|
**改名:npm 包 `museav-cli`,命令 `museav`。** 产品叫 MUSE AV,命令却叫另一个名字,同一个东西两个叫法——这次统一到产品名。
|
package/README.md
CHANGED
|
@@ -172,8 +172,8 @@ museav gen --prompt '把这只鞋抠成透明底' --ref shoe.jpg --transparent
|
|
|
172
172
|
# (白底图看起来完全正常,静默降级只会让你以为提示词没写对,反复重试)
|
|
173
173
|
# · 会强制 PNG 输出:JPEG / 有损 WebP 没有 alpha 通道,装不下透明
|
|
174
174
|
|
|
175
|
-
#
|
|
176
|
-
museav gen --video --prompt '一只橘猫在窗台上伸懒腰,阳光洒进来,电影感' --
|
|
175
|
+
# 文生视频(不传 --model 走 auto 路由;锁死档次用 artsdance-2-0-pro-260801 这类代号,自动轮询直到完成)
|
|
176
|
+
museav gen --video --prompt '一只橘猫在窗台上伸懒腰,阳光洒进来,电影感' --ratio 9:16
|
|
177
177
|
|
|
178
178
|
# 图生视频(--image 传首帧图,自动上传)
|
|
179
179
|
museav gen --video --image logo.png --prompt 'logo 缓缓发光,背景渐暗' --ratio 1:1
|
|
@@ -237,17 +237,24 @@ museav gen --template "$ID" --fields '{"artist":"..."}'
|
|
|
237
237
|
|
|
238
238
|
### 图片逆向 `reverse`
|
|
239
239
|
|
|
240
|
-
|
|
240
|
+
逆推出图 prompt,可以直接拿去再出一张同风格的。**主路是本地 Ollama(qwen3-vl)**:快、零成本、无需登录;本地不可用时自动回落中台 API(SCULPT 六要素:主体/构图/世界观/光影/输出/质感),回落时会明确提示较慢:
|
|
241
241
|
|
|
242
242
|
```bash
|
|
243
|
-
#
|
|
243
|
+
# 本地文件走本地模型(推荐,无需登录)
|
|
244
244
|
museav reverse photo.png
|
|
245
|
+
|
|
246
|
+
# 图片 URL 没法喂本地模型,走中台 API(需登录)
|
|
245
247
|
museav reverse https://example.com/photo.png
|
|
246
248
|
|
|
249
|
+
# 强制走中台 API(跳过本地,慢,需登录)
|
|
250
|
+
museav reverse photo.png --api
|
|
251
|
+
|
|
247
252
|
# 逆向 + 出图,一条龙
|
|
248
253
|
museav gen --prompt "$(museav reverse photo.png)"
|
|
249
254
|
```
|
|
250
255
|
|
|
256
|
+
本地路依赖:Ollama 运行中(`brew services start ollama`)+ 模型在位(`ollama pull qwen3-vl:8b`,换档位设 `MUSEAV_LOCAL_VLM`,自定地址设 `OLLAMA_HOST`)。缺哪个都会提示对应命令并回落 API。
|
|
257
|
+
|
|
251
258
|
分析详情打到 stderr(人看),**stdout 只输出英文 prompt**(机器用,方便管道)。
|
|
252
259
|
|
|
253
260
|
`reverse` **只读图**,不会顺手帮你建模板。要把图做成模板看下一节——中台 2026-08-16 把这两件事
|
|
@@ -315,6 +322,43 @@ museav upload face.png
|
|
|
315
322
|
> `gen --ref` / `gen --video --image` 内部已经自动帮你上传了,不需要先手动跑一次 `upload`。
|
|
316
323
|
> 单独用 `upload` 的场景是:同一张垫图要复用多次,或者你想把 URL 存下来给别的系统用。
|
|
317
324
|
|
|
325
|
+
### 本地图像工具 `compress` / `remove-bg`
|
|
326
|
+
|
|
327
|
+
纯本地、免登录、不消耗中台额度,macOS / Windows / Linux 通用(依赖全走 npm 预编译,无平台特化代码):
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
# 压缩:默认同目录 <名>-min.<格式>,绝不覆写原文件
|
|
331
|
+
museav compress photo.jpg
|
|
332
|
+
museav compress photo.jpg --max-edge 800 --format webp --quality 70 # 597KB → 18KB 级别
|
|
333
|
+
|
|
334
|
+
# 抠图去背景:输出带 alpha 的 PNG(ISNet 模型,本地 ONNX 推理,热跑秒级)
|
|
335
|
+
museav remove-bg product.png
|
|
336
|
+
# stdout 都是产物路径,方便管道串联
|
|
337
|
+
museav gen --prompt "$(museav reverse $(museav remove-bg shoe.png))" # 抠图 → 逆向 → 重生成
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
- `compress` 用 sharp(已是 CLI 可选依赖);`remove-bg` 用 onnxruntime-node + ISNet/U2Net 模型(均 MIT/Apache 兼容许可证,刻意没用 AGPL 的现成 npm 包)。模型首次使用自动下载 ~170MB,缓存到 `~/.museav-models/`(Windows 是 `%USERPROFILE%\.museav-models`)。
|
|
341
|
+
- 两个命令的输出默认带后缀(`-min` / `-nobg`),不会覆盖你的输入文件;要覆盖已存在的输出加 `--overwrite`。
|
|
342
|
+
- 可选依赖缺失时不炸,报一行重装指引(`npm install -g museav-cli`)。
|
|
343
|
+
|
|
344
|
+
### 项目(工作区)与项目素材库 `projects`
|
|
345
|
+
|
|
346
|
+
层级:**平台 → 账户 → 工作区**。一个账户最多 5 个项目,每个项目挂自己的素材库——「人像库的项目出模特图、产品库的项目出电商图」,业务隔离互不污染:
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
museav projects # 列项目(id / 名称 / 出图统计)
|
|
350
|
+
museav projects create --name 'meso猫砂电商' # 新建
|
|
351
|
+
museav projects assets --project meso # 列该项目素材库(stdout: id<TAB>url)
|
|
352
|
+
museav projects assets add ./model.jpg --project meso --name '白T正面' --tag 产品 --tag 电商
|
|
353
|
+
museav projects assets rm <素材id>
|
|
354
|
+
|
|
355
|
+
# 生成归档进项目;垫图直接用素材库里的 URL
|
|
356
|
+
museav gen --project meso --prompt '...' --ref <素材URL>
|
|
357
|
+
museav jobs --project meso # 只看该项目的任务
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
素材是**母版不压缩**(跟垫图上传的视觉压缩是两回事),类型按字节判定(图片/音频/视频)。`--project` 收 id 或名称,重名时提示用 id。**需要中台部署 `workspace-assets` 接口后可用**(含账户 Key 白名单放行)。
|
|
361
|
+
|
|
318
362
|
### 查模型 / 余额
|
|
319
363
|
|
|
320
364
|
```bash
|
|
@@ -417,6 +461,10 @@ console.log(r.sculpt.light) // 光影分析
|
|
|
417
461
|
| `reverse <file\|url>` | 读图,反推 prompt(**只读图**,不建模板) | 英文 prompt |
|
|
418
462
|
| `image-to-template <file\|url>` | 图生模板:读图 + 文字层逆向 + 变量化 → 建成可复用图片模板 | 模板 id(`--no-create` 时是草稿 JSON) |
|
|
419
463
|
| `upload <file>` | 上传素材(图片/音频/视频) | 公网直链 |
|
|
464
|
+
| `compress <file>` | 本地压缩图片(免登录,`--max-edge`/`--quality`/`--format`) | 产物路径 |
|
|
465
|
+
| `remove-bg <file>` | 本地抠图去背景(免登录,输出带 alpha 的 PNG) | 产物路径 |
|
|
466
|
+
| `projects` | 工作区(项目)列表:一账户多项目,各带自己的素材库 | 项目 id 列表 |
|
|
467
|
+
| `projects assets --project` | 项目素材库:ls / add / rm(垫图母版,人像库/产品库各管各的) | `id<TAB>url` 行 |
|
|
420
468
|
| `models` | 可用模型 | 模型名列表 |
|
|
421
469
|
| `balance` | 上游余额 | JSON |
|
|
422
470
|
| `jobs` | 查自己(租户则是自己业务下)的工作流 | JSON 数组 |
|
package/dist/client.d.ts
CHANGED
|
@@ -31,6 +31,30 @@ export interface GenerateOptions {
|
|
|
31
31
|
* · 只派给声明了该能力的上游;一家都没有时返回 400 说明原因,**不会静默出白底图**
|
|
32
32
|
*/
|
|
33
33
|
background?: 'transparent' | 'opaque';
|
|
34
|
+
/** 项目归档:生成结果挂到该工作区(中台仅账户身份收,租户身份忽略) */
|
|
35
|
+
workspace_id?: string;
|
|
36
|
+
}
|
|
37
|
+
/** 工作区(项目):平台账户下的项目容器,素材库挂在它上面(GET/POST /api/workspaces) */
|
|
38
|
+
export interface Workspace {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
brand?: string | null;
|
|
42
|
+
description?: string | null;
|
|
43
|
+
/** 该项目累计提交 / 完成的生成数(列表接口附带的统计) */
|
|
44
|
+
gen_total?: number;
|
|
45
|
+
gen_done?: number;
|
|
46
|
+
created_at?: string;
|
|
47
|
+
}
|
|
48
|
+
/** 工作区素材(GET/POST /api/workspace-assets):项目素材库的一条记录 */
|
|
49
|
+
export interface WorkspaceAsset {
|
|
50
|
+
id: string;
|
|
51
|
+
workspace_id: string;
|
|
52
|
+
media_type: 'image' | 'video' | 'audio';
|
|
53
|
+
cdn_url: string;
|
|
54
|
+
name: string | null;
|
|
55
|
+
tags: string[];
|
|
56
|
+
size_bytes?: number | null;
|
|
57
|
+
created_at?: string;
|
|
34
58
|
}
|
|
35
59
|
/** 图片/文字模板清单项(GET /api/templates,template_type=image|article) */
|
|
36
60
|
export interface TemplateOption {
|
|
@@ -51,6 +75,12 @@ export interface TemplateOption {
|
|
|
51
75
|
model: string;
|
|
52
76
|
prompt_template: string;
|
|
53
77
|
ref_slots?: string[];
|
|
78
|
+
/** 表单字段声明——现行契约放 config 顶层(服务端 validateConfig 读这里) */
|
|
79
|
+
fields?: Array<{
|
|
80
|
+
key: string;
|
|
81
|
+
label: string;
|
|
82
|
+
}>;
|
|
83
|
+
/** 旧存法:fields 曾在 params_json 里,老模板还这么存,读时两种都要兜 */
|
|
54
84
|
params_json?: {
|
|
55
85
|
fields?: Array<{
|
|
56
86
|
key: string;
|
|
@@ -58,6 +88,8 @@ export interface TemplateOption {
|
|
|
58
88
|
placeholder?: string;
|
|
59
89
|
}>;
|
|
60
90
|
};
|
|
91
|
+
duration?: number;
|
|
92
|
+
aspect_ratio?: string;
|
|
61
93
|
is_default?: boolean;
|
|
62
94
|
}>;
|
|
63
95
|
}
|
|
@@ -89,6 +121,8 @@ export interface CreateTemplateInput {
|
|
|
89
121
|
* ratio/duration/model 放在 generation_configs 每项里。 */
|
|
90
122
|
export interface CreateVideoTemplateInput {
|
|
91
123
|
zh_name: string;
|
|
124
|
+
/** 对外调用标识,视频模板硬必填(服务端 validateCore required=['zh_name','slug']),全局唯一 */
|
|
125
|
+
slug: string;
|
|
92
126
|
category?: string;
|
|
93
127
|
description?: string;
|
|
94
128
|
sample_video_url?: string | null;
|
|
@@ -300,6 +334,21 @@ export declare class StudioClient {
|
|
|
300
334
|
}>;
|
|
301
335
|
/** 查单个任务状态 */
|
|
302
336
|
getJob(id: string): Promise<Job>;
|
|
337
|
+
/** 列当前账户的工作区(含生成统计) */
|
|
338
|
+
workspaces(): Promise<Workspace[]>;
|
|
339
|
+
/** 新建工作区(最多 5 个,超了服务端会 400) */
|
|
340
|
+
createWorkspace(name: string): Promise<Workspace>;
|
|
341
|
+
/** 列某工作区的素材库 */
|
|
342
|
+
workspaceAssets(workspaceId: string): Promise<WorkspaceAsset[]>;
|
|
343
|
+
/** 上传素材进工作区素材库。素材是母版,**不做视觉压缩**(fileForm 那套压缩是给模型看的) */
|
|
344
|
+
addWorkspaceAsset(input: {
|
|
345
|
+
file: string;
|
|
346
|
+
workspaceId: string;
|
|
347
|
+
name?: string;
|
|
348
|
+
tags?: string[];
|
|
349
|
+
}): Promise<WorkspaceAsset>;
|
|
350
|
+
/** 删除素材(硬删:R2 对象 + 记录) */
|
|
351
|
+
deleteWorkspaceAsset(id: string): Promise<void>;
|
|
303
352
|
/**
|
|
304
353
|
* 列出当前身份名下的出图工作流(不传 id,走同一个 jobs 端点的集合语义)。
|
|
305
354
|
* 范围由鉴权凭证决定:个人 token 只看得到自己出的图;租户 apiKey 看得到自己业务下的全部记录。
|
|
@@ -329,6 +378,8 @@ export declare class StudioClient {
|
|
|
329
378
|
template_id?: string;
|
|
330
379
|
input?: string | Record<string, string>;
|
|
331
380
|
callback_url?: string;
|
|
381
|
+
/** 项目归档(账户身份才生效) */
|
|
382
|
+
workspace_id?: string;
|
|
332
383
|
}): Promise<{
|
|
333
384
|
jobId: string;
|
|
334
385
|
upstreamTaskId?: string;
|
package/dist/client.js
CHANGED
|
@@ -149,6 +149,9 @@ export class StudioClient {
|
|
|
149
149
|
body.quality = opts.quality;
|
|
150
150
|
if (opts.background)
|
|
151
151
|
body.background = opts.background;
|
|
152
|
+
// 项目归档:中台只对账户身份收 workspace_id(租户身份忽略),CLI 不做二次校验
|
|
153
|
+
if (opts.workspace_id)
|
|
154
|
+
body.workspace_id = opts.workspace_id;
|
|
152
155
|
const r = await this.request('generate', {
|
|
153
156
|
method: 'POST',
|
|
154
157
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -161,6 +164,40 @@ export class StudioClient {
|
|
|
161
164
|
const r = await this.request(`jobs?id=${encodeURIComponent(id)}`);
|
|
162
165
|
return r;
|
|
163
166
|
}
|
|
167
|
+
// ── 工作区(项目)与项目素材库 ──
|
|
168
|
+
// 平台 → 账户 → 工作区三层归属;素材挂工作区,换业务换工作区,互不污染。
|
|
169
|
+
/** 列当前账户的工作区(含生成统计) */
|
|
170
|
+
async workspaces() {
|
|
171
|
+
return this.request('workspaces');
|
|
172
|
+
}
|
|
173
|
+
/** 新建工作区(最多 5 个,超了服务端会 400) */
|
|
174
|
+
async createWorkspace(name) {
|
|
175
|
+
return this.request('workspaces', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }) });
|
|
176
|
+
}
|
|
177
|
+
/** 列某工作区的素材库 */
|
|
178
|
+
async workspaceAssets(workspaceId) {
|
|
179
|
+
return this.request(`workspace-assets?workspace_id=${encodeURIComponent(workspaceId)}`);
|
|
180
|
+
}
|
|
181
|
+
/** 上传素材进工作区素材库。素材是母版,**不做视觉压缩**(fileForm 那套压缩是给模型看的) */
|
|
182
|
+
async addWorkspaceAsset(input) {
|
|
183
|
+
const blob = new Blob([new Uint8Array(readFileSync(input.file))]);
|
|
184
|
+
const fd = new FormData();
|
|
185
|
+
fd.append('file', blob, basename(input.file));
|
|
186
|
+
fd.append('workspace_id', input.workspaceId);
|
|
187
|
+
if (input.name)
|
|
188
|
+
fd.append('name', input.name);
|
|
189
|
+
for (const t of input.tags || [])
|
|
190
|
+
fd.append('tags', t);
|
|
191
|
+
return this.request('workspace-assets', { method: 'POST', body: fd });
|
|
192
|
+
}
|
|
193
|
+
/** 删除素材(硬删:R2 对象 + 记录) */
|
|
194
|
+
async deleteWorkspaceAsset(id) {
|
|
195
|
+
await this.request('workspace-assets', {
|
|
196
|
+
method: 'DELETE',
|
|
197
|
+
headers: { 'Content-Type': 'application/json' },
|
|
198
|
+
body: JSON.stringify({ id }),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
164
201
|
/**
|
|
165
202
|
* 列出当前身份名下的出图工作流(不传 id,走同一个 jobs 端点的集合语义)。
|
|
166
203
|
* 范围由鉴权凭证决定:个人 token 只看得到自己出的图;租户 apiKey 看得到自己业务下的全部记录。
|
|
@@ -216,6 +253,8 @@ export class StudioClient {
|
|
|
216
253
|
body.input = opts.input;
|
|
217
254
|
if (opts.callback_url)
|
|
218
255
|
body.callback_url = opts.callback_url;
|
|
256
|
+
if (opts.workspace_id)
|
|
257
|
+
body.workspace_id = opts.workspace_id;
|
|
219
258
|
const r = await this.request('videos', {
|
|
220
259
|
method: 'POST',
|
|
221
260
|
headers: { 'Content-Type': 'application/json' },
|
package/dist/commands/gen.d.ts
CHANGED
package/dist/commands/gen.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveWorkspace } from './projects.js';
|
|
1
2
|
/** 与中台/各租户后台口径一致:一次最多 5 张参考图 */
|
|
2
3
|
const MAX_REFS = 5;
|
|
3
4
|
export async function gen(client, opts) {
|
|
@@ -44,6 +45,13 @@ export async function gen(client, opts) {
|
|
|
44
45
|
if (refPaths.length) {
|
|
45
46
|
const urls = [];
|
|
46
47
|
for (const [i, refPath] of refPaths.entries()) {
|
|
48
|
+
// http(s) 直链(典型来源:museav projects assets 的素材库 URL)本身就是
|
|
49
|
+
// 中台 CDN 地址,直接当参考图用,不走上传
|
|
50
|
+
if (/^https?:\/\//.test(refPath)) {
|
|
51
|
+
urls.push(refPath);
|
|
52
|
+
process.stderr.write(` 图片${i + 1} 直链: ${refPath}\n`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
47
55
|
process.stderr.write(`上传垫图 [图片${i + 1}] ${refPath} ...\n`);
|
|
48
56
|
const up = await client.uploadRef(refPath);
|
|
49
57
|
urls.push(up.url);
|
|
@@ -52,6 +60,8 @@ export async function gen(client, opts) {
|
|
|
52
60
|
referenceImage = urls[0]; // 兼容:中台单数字段仍收
|
|
53
61
|
referenceImages = urls.length > 1 ? urls : undefined;
|
|
54
62
|
}
|
|
63
|
+
// 项目归档:--project 解析成 workspace_id(名字/ id 都行),租户身份时中台会忽略
|
|
64
|
+
const workspaceId = opts.project ? (await resolveWorkspace(client, opts.project)).id : undefined;
|
|
55
65
|
// ── 视频模式:走 /api/videos 独立链路 ──
|
|
56
66
|
if (opts.video) {
|
|
57
67
|
if (opts.quality)
|
|
@@ -67,6 +77,7 @@ export async function gen(client, opts) {
|
|
|
67
77
|
image_url: referenceImage,
|
|
68
78
|
template_id: opts.template,
|
|
69
79
|
input: templateFields,
|
|
80
|
+
workspace_id: workspaceId,
|
|
70
81
|
});
|
|
71
82
|
process.stderr.write(`视频任务已提交: ${jobId}\n生成中(视频通常 1-5 分钟)...\n`);
|
|
72
83
|
const result = await client.waitVideo(jobId, (status) => {
|
|
@@ -100,6 +111,7 @@ export async function gen(client, opts) {
|
|
|
100
111
|
// 开关 → 枚举:CLI 这层用布尔开关最顺手,中台契约是 background: transparent|opaque
|
|
101
112
|
// (跟上游 gpt-image 的参数同名同值)。不传就不发,行为跟以前完全一样。
|
|
102
113
|
background: opts.transparent ? 'transparent' : undefined,
|
|
114
|
+
workspace_id: workspaceId,
|
|
103
115
|
}, (status) => {
|
|
104
116
|
if (status === 'processing')
|
|
105
117
|
process.stderr.write('生成中...\r');
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** 默认输出路径:同目录 <名字>-<后缀>.<新扩展名>。绝不覆写输入文件 */
|
|
2
|
+
export declare function defaultOut(input: string, suffix: string, newExt?: string): string;
|
|
3
|
+
export interface CompressOpts {
|
|
4
|
+
out?: string;
|
|
5
|
+
maxEdge?: string;
|
|
6
|
+
quality?: string;
|
|
7
|
+
format?: string;
|
|
8
|
+
overwrite?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function compressCmd(input: string, opts: CompressOpts): Promise<void>;
|
|
11
|
+
export interface RemoveBgOpts {
|
|
12
|
+
out?: string;
|
|
13
|
+
model?: string;
|
|
14
|
+
overwrite?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function removeBgCmd(input: string, opts: RemoveBgOpts): Promise<void>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** museav compress / remove-bg —— 本地图像工具箱。
|
|
2
|
+
* 纯本地、免登录、不碰中台;stdout 只出产物路径,统计与进度打 stderr。
|
|
3
|
+
* 代码零平台假设(路径全走 node:path/os,无 shell 展开、无 Unix-only 命令),macOS / Windows 通用。 */
|
|
4
|
+
import { stat, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { basename, dirname, extname, join } from 'node:path';
|
|
6
|
+
import { removeBackgroundLocal, BG_MODELS } from '../local-bg.js';
|
|
7
|
+
async function fileExists(path) {
|
|
8
|
+
try {
|
|
9
|
+
return (await stat(path)).isFile();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function fmtBytes(n) {
|
|
16
|
+
return n >= 1048576 ? `${(n / 1048576).toFixed(2)}MB` : `${(n / 1024).toFixed(1)}KB`;
|
|
17
|
+
}
|
|
18
|
+
/** 默认输出路径:同目录 <名字>-<后缀>.<新扩展名>。绝不覆写输入文件 */
|
|
19
|
+
export function defaultOut(input, suffix, newExt) {
|
|
20
|
+
const ext = newExt || extname(input).slice(1) || 'png';
|
|
21
|
+
return join(dirname(input), `${basename(input, extname(input))}-${suffix}.${ext}`);
|
|
22
|
+
}
|
|
23
|
+
export async function compressCmd(input, opts) {
|
|
24
|
+
if (!(await fileExists(input)))
|
|
25
|
+
throw new Error(`文件不存在: ${input}`);
|
|
26
|
+
let sharp;
|
|
27
|
+
try {
|
|
28
|
+
const m = await import('sharp');
|
|
29
|
+
sharp = m.default ?? m;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw new Error('sharp 不可用(压缩依赖它)。重装 CLI 即可补上:npm install -g museav-cli');
|
|
33
|
+
}
|
|
34
|
+
const format = (opts.format || '').toLowerCase();
|
|
35
|
+
if (format && !['jpg', 'png', 'webp'].includes(format))
|
|
36
|
+
throw new Error('--format 只支持 jpg / png / webp');
|
|
37
|
+
const quality = opts.quality ? Number(opts.quality) : 82;
|
|
38
|
+
if (!Number.isFinite(quality) || quality < 1 || quality > 100)
|
|
39
|
+
throw new Error('--quality 必须是 1-100');
|
|
40
|
+
const maxEdge = opts.maxEdge ? Number(opts.maxEdge) : 0;
|
|
41
|
+
if (opts.maxEdge && (!Number.isFinite(maxEdge) || maxEdge < 16))
|
|
42
|
+
throw new Error('--max-edge 至少 16px');
|
|
43
|
+
const meta = await sharp(input).metadata();
|
|
44
|
+
// 不指定 --format 时保持原格式;不在三之列的(tiff/bmp/heic…)统一转 jpg
|
|
45
|
+
const srcFormat = String(meta.format || '');
|
|
46
|
+
const target = format || (srcFormat === 'png' ? 'png' : srcFormat === 'webp' ? 'webp' : 'jpg');
|
|
47
|
+
let pipeline = sharp(input).rotate(); // 尊重 EXIF 方向
|
|
48
|
+
if (maxEdge)
|
|
49
|
+
pipeline = pipeline.resize({ width: maxEdge, height: maxEdge, fit: 'inside' });
|
|
50
|
+
if (target === 'jpg')
|
|
51
|
+
pipeline = pipeline.jpeg({ quality, mozjpeg: true });
|
|
52
|
+
else if (target === 'webp')
|
|
53
|
+
pipeline = pipeline.webp({ quality });
|
|
54
|
+
else
|
|
55
|
+
pipeline = pipeline.png({ compressionLevel: 9 });
|
|
56
|
+
const outPath = opts.out || defaultOut(input, 'min', target === 'jpg' && srcFormat === 'jpeg' ? 'jpg' : target);
|
|
57
|
+
if ((await fileExists(outPath)) && !opts.overwrite) {
|
|
58
|
+
throw new Error(`输出已存在(用 --overwrite 覆盖或 --out 换路径): ${outPath}`);
|
|
59
|
+
}
|
|
60
|
+
const buf = await pipeline.toBuffer();
|
|
61
|
+
await writeFile(outPath, buf);
|
|
62
|
+
const before = (await stat(input)).size;
|
|
63
|
+
process.stderr.write(`✅ ${fmtBytes(before)} → ${fmtBytes(buf.length)}(省 ${Math.max(0, Math.round((1 - buf.length / before) * 100))}%,${target.toUpperCase()})\n`);
|
|
64
|
+
console.log(outPath);
|
|
65
|
+
}
|
|
66
|
+
export async function removeBgCmd(input, opts) {
|
|
67
|
+
if (!(await fileExists(input)))
|
|
68
|
+
throw new Error(`文件不存在: ${input}`);
|
|
69
|
+
const modelKey = (opts.model || 'isnet');
|
|
70
|
+
if (!(modelKey in BG_MODELS))
|
|
71
|
+
throw new Error(`--model 只支持 ${Object.keys(BG_MODELS).join(' / ')}`);
|
|
72
|
+
const start = Date.now();
|
|
73
|
+
const png = await removeBackgroundLocal(input, modelKey);
|
|
74
|
+
const outPath = opts.out || defaultOut(input, 'nobg', 'png');
|
|
75
|
+
if ((await fileExists(outPath)) && !opts.overwrite) {
|
|
76
|
+
throw new Error(`输出已存在(用 --overwrite 覆盖或 --out 换路径): ${outPath}`);
|
|
77
|
+
}
|
|
78
|
+
await writeFile(outPath, png);
|
|
79
|
+
process.stderr.write(`✅ 抠图完成(${BG_MODELS[modelKey].label},用时 ${((Date.now() - start) / 1000).toFixed(1)}s,${fmtBytes(png.length)})\n`);
|
|
80
|
+
console.log(outPath);
|
|
81
|
+
}
|
package/dist/commands/jobs.d.ts
CHANGED
package/dist/commands/jobs.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveWorkspace } from './projects.js';
|
|
1
2
|
const STATUS_MARK = {
|
|
2
3
|
pending: '⏳',
|
|
3
4
|
processing: '🔄',
|
|
@@ -6,7 +7,12 @@ const STATUS_MARK = {
|
|
|
6
7
|
};
|
|
7
8
|
export async function jobs(client, opts) {
|
|
8
9
|
const limit = opts.limit ? Number(opts.limit) : 20;
|
|
9
|
-
|
|
10
|
+
let list = await client.listJobs({ limit, status: opts.status });
|
|
11
|
+
// --project 客户端过滤:服务端 jobs 不认 workspace 参数,在最近 50 条内筛
|
|
12
|
+
if (opts.project) {
|
|
13
|
+
const ws = await resolveWorkspace(client, opts.project);
|
|
14
|
+
list = list.filter((j) => j.workspace_id === ws.id);
|
|
15
|
+
}
|
|
10
16
|
if (!list.length) {
|
|
11
17
|
process.stderr.write('没有找到工作流记录\n');
|
|
12
18
|
console.log('[]');
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** museav projects —— 工作区(项目)与项目素材库。
|
|
2
|
+
* 层级:平台 → 账户 → 工作区;素材挂工作区,业务隔离互不污染。
|
|
3
|
+
* 「人像库的工作区出模特图、产品库的工作区出电商图」的载体就是这里。 */
|
|
4
|
+
import type { StudioClient, Workspace } from '../client.js';
|
|
5
|
+
/** --project <id|名> 的解析:id 精确命中,否则按名称匹配;找不到/歧义时把可选项列出来 */
|
|
6
|
+
export declare function resolveWorkspace(client: StudioClient, idOrName: string): Promise<Workspace>;
|
|
7
|
+
export declare function projects(client: StudioClient): Promise<void>;
|
|
8
|
+
export declare function createProject(client: StudioClient, opts: {
|
|
9
|
+
name: string;
|
|
10
|
+
}): Promise<void>;
|
|
11
|
+
export declare function listAssets(client: StudioClient, opts: {
|
|
12
|
+
project?: string;
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
export declare function addAsset(client: StudioClient, file: string, opts: {
|
|
15
|
+
project?: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
tag?: string[];
|
|
18
|
+
}): Promise<void>;
|
|
19
|
+
export declare function removeAsset(client: StudioClient, opts: {
|
|
20
|
+
id: string;
|
|
21
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** --project <id|名> 的解析:id 精确命中,否则按名称匹配;找不到/歧义时把可选项列出来 */
|
|
2
|
+
export async function resolveWorkspace(client, idOrName) {
|
|
3
|
+
const list = await client.workspaces();
|
|
4
|
+
const key = String(idOrName || '').trim();
|
|
5
|
+
if (!key)
|
|
6
|
+
throw new Error('缺少 --project(工作区 id 或名称)');
|
|
7
|
+
const byId = list.find((w) => w.id === key);
|
|
8
|
+
if (byId)
|
|
9
|
+
return byId;
|
|
10
|
+
const byName = list.filter((w) => w.name === key);
|
|
11
|
+
if (byName.length === 1)
|
|
12
|
+
return byName[0];
|
|
13
|
+
if (byName.length > 1)
|
|
14
|
+
throw new Error(`重名工作区「${key}」,请用 id 指定:\n${list.map((w) => ` ${w.id} ${w.name}`).join('\n')}`);
|
|
15
|
+
throw new Error(`没有工作区「${key}」。现有:\n${list.map((w) => ` ${w.id} ${w.name}`).join('\n')}\n(museav projects create --name 可新建)`);
|
|
16
|
+
}
|
|
17
|
+
export async function projects(client) {
|
|
18
|
+
const list = await client.workspaces();
|
|
19
|
+
if (!list.length) {
|
|
20
|
+
process.stderr.write('还没有工作区(museav projects create --name 新建)\n');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
process.stderr.write(`工作区(${list.length} 个):\n`);
|
|
24
|
+
for (const w of list) {
|
|
25
|
+
process.stderr.write(` ${w.id} ${w.name.padEnd(16)} 出图 ${w.gen_done ?? 0}/${w.gen_total ?? 0}${w.brand ? ` brand:${w.brand}` : ''}\n`);
|
|
26
|
+
}
|
|
27
|
+
process.stderr.write(`\n素材库: museav projects assets --project <id|名>\n出图归档: museav gen --project <id|名> ...\n`);
|
|
28
|
+
// stdout 只出 id,便于脚本解析
|
|
29
|
+
console.log(list.map((w) => w.id).join('\n'));
|
|
30
|
+
}
|
|
31
|
+
export async function createProject(client, opts) {
|
|
32
|
+
const name = (opts.name || '').trim();
|
|
33
|
+
if (!name)
|
|
34
|
+
throw new Error('--name 必填');
|
|
35
|
+
const row = await client.createWorkspace(name);
|
|
36
|
+
process.stderr.write(`✅ 工作区已建:${row.id} ${row.name}\n`);
|
|
37
|
+
process.stderr.write(`传素材: museav projects assets add <file> --project ${row.id}\n`);
|
|
38
|
+
console.log(row.id);
|
|
39
|
+
}
|
|
40
|
+
export async function listAssets(client, opts) {
|
|
41
|
+
const ws = await resolveWorkspace(client, opts.project || '');
|
|
42
|
+
const assets = await client.workspaceAssets(ws.id);
|
|
43
|
+
process.stderr.write(`「${ws.name}」素材库(${assets.length} 条):\n`);
|
|
44
|
+
for (const a of assets) {
|
|
45
|
+
const tag = a.tags?.length ? `[${a.tags.join(',')}]` : '';
|
|
46
|
+
process.stderr.write(` ${a.id} ${(a.name || '(未命名)').padEnd(16)} ${a.media_type.padEnd(5)} ${tag}\n`);
|
|
47
|
+
process.stderr.write(` ${a.cdn_url}\n`);
|
|
48
|
+
}
|
|
49
|
+
process.stderr.write(`\n垫图出图: museav gen --project ${ws.id} --ref <素材URL> --prompt '...'\n`);
|
|
50
|
+
// stdout:id<TAB>url 每行一条,agent 拿去直接当 --ref 用
|
|
51
|
+
console.log(assets.map((a) => `${a.id}\t${a.cdn_url}`).join('\n'));
|
|
52
|
+
}
|
|
53
|
+
export async function addAsset(client, file, opts) {
|
|
54
|
+
const ws = await resolveWorkspace(client, opts.project || '');
|
|
55
|
+
const row = await client.addWorkspaceAsset({
|
|
56
|
+
file,
|
|
57
|
+
workspaceId: ws.id,
|
|
58
|
+
name: opts.name,
|
|
59
|
+
tags: opts.tag || [],
|
|
60
|
+
});
|
|
61
|
+
process.stderr.write(`✅ 已入「${ws.name}」素材库:${row.name || '(未命名)'} ${row.media_type}\n`);
|
|
62
|
+
process.stderr.write(`${row.cdn_url}\n`);
|
|
63
|
+
console.log(row.cdn_url);
|
|
64
|
+
}
|
|
65
|
+
export async function removeAsset(client, opts) {
|
|
66
|
+
await client.deleteWorkspaceAsset(opts.id);
|
|
67
|
+
process.stderr.write(`✅ 素材已删:${opts.id}\n`);
|
|
68
|
+
console.log(opts.id);
|
|
69
|
+
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
/** museav reverse —— 图片逆向(SCULPT 六要素反推 prompt
|
|
1
|
+
/** museav reverse —— 图片逆向(SCULPT 六要素反推 prompt)。
|
|
2
|
+
* 主路是本地 Ollama(qwen3-vl),快、零成本、无需登录;中台 API 是回落路,走回落时会明确提示较慢。
|
|
3
|
+
* client 懒构造(getClient):本地路成功就完全不碰中台凭证。
|
|
4
|
+
* 本地系统的 AI 能力统一收口在这个 CLI,reverse 是第一个本地化的能力。 */
|
|
2
5
|
import type { StudioClient } from '../client.js';
|
|
3
|
-
export declare function reverse(
|
|
6
|
+
export declare function reverse(getClient: () => StudioClient, input: string, opts?: {
|
|
7
|
+
api?: boolean;
|
|
8
|
+
}): Promise<void>;
|
package/dist/commands/reverse.js
CHANGED
|
@@ -1,7 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { checkLocalVlm, reverseLocally, LOCAL_VLM_MODEL } from '../local-vision.js';
|
|
2
|
+
export async function reverse(getClient, input, opts = {}) {
|
|
3
3
|
const isUrl = /^https?:\/\//.test(input);
|
|
4
|
+
if (!opts.api && !isUrl) {
|
|
5
|
+
const status = await checkLocalVlm();
|
|
6
|
+
if (status.running && status.modelPresent) {
|
|
7
|
+
try {
|
|
8
|
+
const start = Date.now();
|
|
9
|
+
const result = await reverseLocally(input);
|
|
10
|
+
process.stderr.write(`✓ 本地 Ollama(${LOCAL_VLM_MODEL})用时 ${((Date.now() - start) / 1000).toFixed(1)}s\n`);
|
|
11
|
+
renderReverse(result);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
catch (e) {
|
|
15
|
+
process.stderr.write(`⚠ 本地读图失败(${e instanceof Error ? e.message : e}),回落中台 API —— 速度较慢,请耐心等待\n`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
process.stderr.write(`⚠ 本地读图不可用(${status.reason}),回落中台 API —— 速度较慢,请耐心等待\n`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
else if (!opts.api && isUrl) {
|
|
23
|
+
process.stderr.write(`ℹ URL 输入走中台 API(本地路只收文件路径)\n`);
|
|
24
|
+
}
|
|
25
|
+
const client = getClient();
|
|
4
26
|
const result = await client.reverse(isUrl ? { imageUrl: input } : { file: input });
|
|
27
|
+
renderReverse(result);
|
|
28
|
+
}
|
|
29
|
+
/** 两条路产出同构,渲染只写一份 */
|
|
30
|
+
function renderReverse(result) {
|
|
5
31
|
process.stderr.write(`✅ 逆向完成\n\n`);
|
|
6
32
|
process.stderr.write(`风格: ${result.zh_name || '-'} 比例: ${result.aspect_ratio}\n`);
|
|
7
33
|
process.stderr.write(`标签: ${result.style_tags.join(', ')}\n\n`);
|