museav-cli 2.5.0 → 2.8.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 +16 -0
- package/CHANGELOG.md +67 -0
- package/README.md +61 -0
- package/dist/client.d.ts +31 -3
- package/dist/client.js +50 -3
- package/dist/commands/balance.js +5 -2
- package/dist/commands/img-tools.js +2 -1
- package/dist/commands/poster-templates.d.ts +11 -0
- package/dist/commands/poster-templates.js +23 -0
- package/dist/commands/speak.d.ts +9 -0
- package/dist/commands/speak.js +41 -0
- package/dist/commands/stickers.d.ts +10 -0
- package/dist/commands/stickers.js +22 -0
- package/dist/commands/upload.d.ts +4 -1
- package/dist/commands/upload.js +15 -2
- package/dist/compress.js +8 -0
- package/dist/index.js +40 -3
- package/dist/local-bg.d.ts +23 -0
- package/dist/local-bg.js +68 -19
- package/dist/mimo-speech.d.ts +31 -0
- package/dist/mimo-speech.js +127 -0
- package/package.json +1 -1
- package/src/client.ts +64 -5
- package/src/commands/balance.ts +5 -2
- package/src/commands/img-tools.ts +2 -1
- package/src/commands/poster-templates.ts +33 -0
- package/src/commands/speak.ts +54 -0
- package/src/commands/stickers.ts +29 -0
- package/src/commands/upload.ts +18 -2
- package/src/compress.ts +8 -0
- package/src/index.ts +47 -3
- package/src/local-bg.ts +66 -18
- package/src/mimo-speech.ts +139 -0
package/AGENTS.md
CHANGED
|
@@ -56,6 +56,22 @@ museav gen --prompt "$(museav reverse ./photo.png)"
|
|
|
56
56
|
museav image-to-template ./poster.jpg --name '暗金演唱会主视觉' --variables title,subject,location
|
|
57
57
|
museav image-to-template ./poster.jpg --no-create # dry run: draft JSON on stdout, nothing created
|
|
58
58
|
|
|
59
|
+
# Text-to-speech / speech-to-text (Xiaomi MiMo). These talk DIRECTLY to the upstream,
|
|
60
|
+
# NOT through the platform — so they need MIMO_API_KEY and ignore your login/apiKey entirely.
|
|
61
|
+
# Reason: the platform's audio pipeline isn't wired up yet, and this capability is
|
|
62
|
+
# internal-only for now (tenants don't have this key, so they can't reach it).
|
|
63
|
+
# export MIMO_API_KEY=... or cs kyvault run --env MIMO_API_KEY=secret://mimo/api-key -- museav ...
|
|
64
|
+
# stdout: the written .wav path (speak) / the recognized text (transcribe).
|
|
65
|
+
museav speak '声影成诗,一念成像。' # preset voice (Chloe)
|
|
66
|
+
museav speak '欢迎收听' --design '低沉沙哑的中年男声' # invent a voice from a description
|
|
67
|
+
museav speak '这句换个音色' --clone ./sample.wav # clone the voice in sample.wav
|
|
68
|
+
museav speak '慢一点念' --instruction '语速放慢,温柔一些' # style/tone instruction
|
|
69
|
+
museav transcribe ./recording.wav
|
|
70
|
+
|
|
71
|
+
# ⚠️ transcribe accuracy wobbles on homophones — the same synthesized line came back as
|
|
72
|
+
# 「声影成诗,一念成相」 once and 「上庸城失,一面呈象」 another time. Don't feed its output
|
|
73
|
+
# into anything that bills, stores, or branches on exact text without a human check.
|
|
74
|
+
|
|
59
75
|
# Upload a file (image/audio/video; type is detected from the bytes, not the extension)
|
|
60
76
|
museav upload ./face.png
|
|
61
77
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,72 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.8.0 (2026-08-27)
|
|
4
|
+
|
|
5
|
+
### 新增:贴图素材 + 版式模板命令(租户级资产,给封面/海报工具用)
|
|
6
|
+
|
|
7
|
+
两个新命令,底层调中台 `/api/stickers` 和 `/api/poster-templates`,账户 Key / 租户 Key 均可(落到本租户)。
|
|
8
|
+
|
|
9
|
+
- `museav stickers`:列出本租户贴图素材(PNG 透明装饰图)
|
|
10
|
+
- `museav stickers add <file> --name <名称>`:上传贴图(不压缩,保留透明通道)
|
|
11
|
+
- `museav poster-templates`:列出版式模板(封面底图 + 固定描述)
|
|
12
|
+
- `museav poster-templates add <file> --name <名称> --prompt <描述>`:保存版式(`{城市}` `{明星}` 占位符会被替换)
|
|
13
|
+
|
|
14
|
+
背景:好易美(hym)的闲鱼封面工具此前版式存学员本机桌面、贴图只在后台本地,无法共享。
|
|
15
|
+
现在贴图素材 + 版式模板收归中台,CLI 一条命令加载/保存,学员间共享。
|
|
16
|
+
|
|
17
|
+
## 2.7.0 (2026-08-27)
|
|
18
|
+
|
|
19
|
+
### 修复:`remove-bg` 抠不出主体(三个静默 bug)
|
|
20
|
+
|
|
21
|
+
抠图一直输出条纹残影、主体几乎全被抠掉。排查出三个独立 bug,共同点是**全都不报错**,
|
|
22
|
+
只是安静地输出一张糊掉的 mask —— 这类 bug 只能靠拿基准实现(rembg)逐项对齐才查得出来。
|
|
23
|
+
|
|
24
|
+
1. **主因:`sharp` 对单通道 raw 做 `resize` 后会返回 3 通道**(灰度被展开成 RGB)。
|
|
25
|
+
代码仍按 `maskFull[i]` 索引,等于以 1/3 的步长错位采样,于是每隔几行错位一次 ——
|
|
26
|
+
这就是条纹残影的来源。现改为 `toColourspace('b-w')` + 按实际 `channels` 步长索引,两道保险。
|
|
27
|
+
2. **输入尺寸与归一化参数被硬编码成一套**(1024 + `(x/255-0.5)/0.5`),而每个模型都不同:
|
|
28
|
+
`isnet` 是 1024 / mean .5 / **std 1.0**,`u2net` 是 **320** / ImageNet 参数。
|
|
29
|
+
于是 u2net 直接崩(`Got: 1024 Expected: 320`),isnet 因 std 用 0.5 而非 1.0
|
|
30
|
+
把输入值域放大一倍。参数已改为随模型定义。
|
|
31
|
+
3. **归一化分母用固定 255**,而 rembg 用的是该图的最大像素值(`im_ary / max(im_ary)`)。
|
|
32
|
+
偏暗的图用 255 归一化会让输入分布整体偏小。
|
|
33
|
+
|
|
34
|
+
### 新增:BiRefNet-Lite 模型,并设为 `remove-bg` 默认
|
|
35
|
+
|
|
36
|
+
`--model birefnet`(214MB,首次自动下载)。实测同一张白猫照片,主体召回:
|
|
37
|
+
**birefnet 57% / u2net 25% / isnet 10%** —— 毛发、白色主体、低对比度背景全面更好。
|
|
38
|
+
|
|
39
|
+
注意 BiRefNet 输出的是 **logits,要先过 sigmoid** 才是概率(ISNet/U2Net 的输出已在 0-1 区间)。
|
|
40
|
+
漏掉这步同样不报错,只会得到一张几乎全是半透明的 mask,所以 `sigmoid` 做成了模型属性。
|
|
41
|
+
|
|
42
|
+
修完三个模型的输出与 rembg 基准逐一对齐(57.0/57.1、10.5/10.9、25.1/25.1)。
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
## 2.6.0 (2026-08-21)
|
|
46
|
+
|
|
47
|
+
### 新增:语音能力(`speak` / `transcribe`)
|
|
48
|
+
|
|
49
|
+
接入小米 MiMo 的语音档,**直连上游、不走中台身份**,只认 `MIMO_API_KEY` 环境变量。
|
|
50
|
+
中台的出音链路(`media_type=audio` 的路由与落盘)还没接完,而这批能力目前内部用——
|
|
51
|
+
没有那把 key 的人(包括租户)用不了,所以不需要额外做权限控制。
|
|
52
|
+
|
|
53
|
+
- `speak <text>`:文字转语音,输出 24kHz / 16bit 单声道 WAV,stdout 只打印文件路径。
|
|
54
|
+
三种音色来源,给哪个参数走哪条:默认预置音色(`--voice`,默认 Chloe)、
|
|
55
|
+
`--design` 一句话描述当场造一个、`--clone <file>` 拿一段音频克隆它的音色。
|
|
56
|
+
`--instruction` 可叠加语气/风格指令。
|
|
57
|
+
- `transcribe <audio>`:语音转文字,stdout 只打印识别结果。
|
|
58
|
+
|
|
59
|
+
协议上有四个反直觉的点,都写进了 `src/mimo-speech.ts` 的文件头(实测踩出来的,写错不报错、
|
|
60
|
+
只是拿不到音频):合成不走 `/v1/audio/speech`(OpenAI 那套音频端点全 404,四种能力共用
|
|
61
|
+
`/v1/chat/completions`);**待合成文本要放 assistant 角色**,放 user 会得到一段「回答」而不是朗读;
|
|
62
|
+
音频是 base64 回在 `message.audio.data`;识别的输入音频在 user 的 content 数组里且要裸 base64。
|
|
63
|
+
|
|
64
|
+
### 已知限制
|
|
65
|
+
|
|
66
|
+
**识别结果的同音字会飘。** 同一段合成音频,一次识别成「声影成诗,一念成相」,另一次成
|
|
67
|
+
「上庸城失,一面呈象」。CLI 会在 stderr 提醒,但不要把它的输出直接用在计费、入库或需要
|
|
68
|
+
精确匹配的判断上。
|
|
69
|
+
|
|
3
70
|
## 2.5.0 (2026-08-17)
|
|
4
71
|
|
|
5
72
|
### 修复
|
package/README.md
CHANGED
|
@@ -102,9 +102,14 @@
|
|
|
102
102
|
| | 抠图去背景(输出透明 PNG) | `remove-bg` |
|
|
103
103
|
| | **放大清晰度**(2M → 10M+ 级) | `upscale` |
|
|
104
104
|
| | 去水印 | `remove-watermark` |
|
|
105
|
+
| **语音**(直连小米 MiMo,需 `MIMO_API_KEY`) | 文字转语音 / 音色设计 / 音色克隆 | `speak` |
|
|
106
|
+
| | 语音转文字 | `transcribe` |
|
|
105
107
|
| **素材与统计** | 上传素材 / 查任务 / 查余额 / 查模型 / 查身份 | `upload` / `jobs` / `balance` / `models` / `whoami` |
|
|
106
108
|
|
|
107
109
|
> 本地工具(`compress` / `remove-bg` / `upscale` / `remove-watermark`)**不用登录、不花一分钱**,装了就能用;其余命令需要一个凭证(个人 `login` 或租户 apiKey)。
|
|
110
|
+
>
|
|
111
|
+
> 语音(`speak` / `transcribe`)是第三种情况:**不走中台身份**,直连小米 MiMo,只认 `MIMO_API_KEY` 环境变量。
|
|
112
|
+
> 中台的出音链路还没接完,这批能力目前内部用,没有那把 key 的人(包括租户)用不了。
|
|
108
113
|
|
|
109
114
|
---
|
|
110
115
|
|
|
@@ -328,6 +333,48 @@ museav gen --template "$ID" --fields '{"title":"新的主标题"}'
|
|
|
328
333
|
- **降级不是失败**:文字层逆向 / 变量化 / 建模板任一步出问题,读图结果(SCULPT、prompt)照常给你,
|
|
329
334
|
只是没有模具。命令会明确告诉你卡在哪一步。
|
|
330
335
|
|
|
336
|
+
### 文字转语音 `speak` / 语音转文字 `transcribe`
|
|
337
|
+
|
|
338
|
+
直连小米 MiMo,**不用 `museav login`**,只要一个环境变量:
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
export MIMO_API_KEY=...
|
|
342
|
+
# 或者不落盘,用密钥库注入一次性子进程
|
|
343
|
+
cs kyvault run --env MIMO_API_KEY=secret://mimo/api-key -- museav speak '声影成诗'
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
三种音色来源,给了哪个参数就走哪条路:
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
# 预置音色(默认 Chloe)
|
|
350
|
+
museav speak '声影成诗,一念成像。' --out hello.wav
|
|
351
|
+
|
|
352
|
+
# 音色设计:一句话描述,当场造一个音色
|
|
353
|
+
museav speak '欢迎收听山鬼电台' --design '低沉沙哑的中年男声,像深夜电台'
|
|
354
|
+
|
|
355
|
+
# 音色克隆:拿一段音频当样本,复刻它的音色
|
|
356
|
+
museav speak '这句换个音色来念' --clone ./sample.wav
|
|
357
|
+
|
|
358
|
+
# 语气/风格指令,三种模式都能叠加
|
|
359
|
+
museav speak '慢一点念这句' --instruction '语速放慢,温柔一些'
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
输出是 24kHz / 16bit 单声道 WAV,stdout 只打印文件路径(方便直接接管道):
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
museav upload "$(museav speak '开场白')" # 合成完直接上传到中台图库
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
语音转文字:
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
museav transcribe ./recording.wav # stdout 只有识别出的文本
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
> ⚠️ **识别结果的同音字会飘。** 同一段合成音频,一次识别成「声影成诗,一念成相」,
|
|
375
|
+
> 另一次成「上庸城失,一面呈象」。别把它的输出直接用在计费、入库或需要精确匹配的判断上,
|
|
376
|
+
> 重要场景请人工核对一遍。
|
|
377
|
+
|
|
331
378
|
### 上传素材 `upload`
|
|
332
379
|
|
|
333
380
|
图片、音频、视频都能传,中台按**文件字节内容**判类型(不看扩展名,也不信客户端声明的 MIME),
|
|
@@ -345,6 +392,20 @@ museav upload face.png
|
|
|
345
392
|
> `gen --ref` / `gen --video --image` 内部已经自动帮你上传了,不需要先手动跑一次 `upload`。
|
|
346
393
|
> 单独用 `upload` 的场景是:同一张垫图要复用多次,或者你想把 URL 存下来给别的系统用。
|
|
347
394
|
|
|
395
|
+
**把外面做好的成品收进账户**:加 `--to-works`。
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
museav upload my-clip.mp4 --to-works # 收进「我的作品」
|
|
399
|
+
museav upload my-clip.mp4 --to-works --workspace <id> # 顺便归档到某个项目
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
区别在于**落不落库**:默认只把文件存进图库、回一个直链(参考图就该这样,它是原料不是成品);
|
|
403
|
+
加了 `--to-works` 会额外记一条作品,这样它才会出现在网页的「我的作品」、后台画廊和项目归档里。
|
|
404
|
+
不加的话文件只存在存储桶里,界面上哪儿都找不到它。
|
|
405
|
+
|
|
406
|
+
只对**账户身份**生效(`museav login` 或账户 Key):作品要归到具体某个人头上,
|
|
407
|
+
租户 Key 没有具体的人,中台会忽略这个参数,CLI 也会明确告诉你没记成作品。
|
|
408
|
+
|
|
348
409
|
### 本地图像工具 `compress` / `remove-bg`
|
|
349
410
|
|
|
350
411
|
纯本地、免登录、不消耗中台额度,macOS / Windows / Linux 通用(依赖全走 npm 预编译,无平台特化代码):
|
package/dist/client.d.ts
CHANGED
|
@@ -277,8 +277,13 @@ export interface ModelOption {
|
|
|
277
277
|
description?: string;
|
|
278
278
|
}
|
|
279
279
|
export interface Balance {
|
|
280
|
-
/**
|
|
281
|
-
|
|
280
|
+
/** 余额(¥)。中台 2026-08-21 起发这个字段名 */
|
|
281
|
+
balance_cny?: number;
|
|
282
|
+
/**
|
|
283
|
+
* 同一个数的旧字段名,中台仍在双发。字段名带 usd 纯属历史遗留,值一直是人民币——
|
|
284
|
+
* 中台侧不存在汇率换算。老版本 CLI 只认这个名字,所以中台不会立刻停发。
|
|
285
|
+
*/
|
|
286
|
+
balance_usd?: number;
|
|
282
287
|
/** 租户加价率(0.2 = 加价 20%) */
|
|
283
288
|
markup_pct: number;
|
|
284
289
|
checked_at: string;
|
|
@@ -332,6 +337,18 @@ export declare class StudioClient {
|
|
|
332
337
|
createTemplate(input: CreateTemplateInput): Promise<TemplateOption>;
|
|
333
338
|
/** 新建视频模板。归属同图片模板:租户 apiKey 自动归租户,平台管理员归平台共享 */
|
|
334
339
|
createVideoTemplate(input: CreateVideoTemplateInput): Promise<TemplateOption>;
|
|
340
|
+
/** 贴图素材清单(租户级资产,服务端已按调用者权限过滤)。PNG 装饰图,叠加在海报/封面上 */
|
|
341
|
+
stickers(): Promise<any[]>;
|
|
342
|
+
/** 上传贴图素材(不压缩——透明 PNG 压缩会破坏透明通道) */
|
|
343
|
+
createSticker(filePath: string, name: string): Promise<any>;
|
|
344
|
+
/** 删除贴图素材 */
|
|
345
|
+
deleteSticker(id: string): Promise<void>;
|
|
346
|
+
/** 版式模板清单(租户级资产):封面底图 + 固定描述({城市}{明星} 占位符) */
|
|
347
|
+
posterTemplates(): Promise<any[]>;
|
|
348
|
+
/** 上传版式模板(不压缩,保留封面底图原样) */
|
|
349
|
+
createPosterTemplate(filePath: string, name: string, prompt: string): Promise<any>;
|
|
350
|
+
/** 删除版式模板 */
|
|
351
|
+
deletePosterTemplate(id: string): Promise<void>;
|
|
335
352
|
/** 提交出图任务,立即返回 jobId */
|
|
336
353
|
generate(opts: GenerateOptions): Promise<{
|
|
337
354
|
jobId: string;
|
|
@@ -418,10 +435,21 @@ export declare class StudioClient {
|
|
|
418
435
|
* 上传素材。图片会先压到视觉模型够用的尺寸再传(见 compress.ts)——
|
|
419
436
|
* 参考图是给模型看的,不是留档,原图直传只会拖慢上传和解析。
|
|
420
437
|
*/
|
|
421
|
-
|
|
438
|
+
/**
|
|
439
|
+
* 上传素材。默认只存文件、回直链(参考图/垫图就该这样)。
|
|
440
|
+
*
|
|
441
|
+
* asWork=true 时另外记一条作品:不落库的话文件只存在 R2 里,
|
|
442
|
+
* 作品页、后台画廊、项目归档全都看不见它 —— 「传到我的账户」就没发生。
|
|
443
|
+
* 只对账户身份生效(作品要归到具体某个人头上),租户 key 传了也会被中台忽略。
|
|
444
|
+
*/
|
|
445
|
+
uploadRef(filePath: string, opts?: {
|
|
446
|
+
asWork?: boolean;
|
|
447
|
+
workspaceId?: string;
|
|
448
|
+
}): Promise<{
|
|
422
449
|
url: string;
|
|
423
450
|
media_type?: string;
|
|
424
451
|
mime?: string;
|
|
452
|
+
job_id?: string | null;
|
|
425
453
|
}>;
|
|
426
454
|
/**
|
|
427
455
|
* 图片转模板(POST /api/image-to-template):一张图 → 一个可复用的图片模板。
|
package/dist/client.js
CHANGED
|
@@ -125,6 +125,41 @@ export class StudioClient {
|
|
|
125
125
|
});
|
|
126
126
|
return r.row;
|
|
127
127
|
}
|
|
128
|
+
/** 贴图素材清单(租户级资产,服务端已按调用者权限过滤)。PNG 装饰图,叠加在海报/封面上 */
|
|
129
|
+
async stickers() {
|
|
130
|
+
const r = await this.request('stickers');
|
|
131
|
+
return Array.isArray(r) ? r : [];
|
|
132
|
+
}
|
|
133
|
+
/** 上传贴图素材(不压缩——透明 PNG 压缩会破坏透明通道) */
|
|
134
|
+
async createSticker(filePath, name) {
|
|
135
|
+
const blob = new Blob([new Uint8Array(readFileSync(filePath))]);
|
|
136
|
+
const fd = new FormData();
|
|
137
|
+
fd.append('file', blob, basename(filePath));
|
|
138
|
+
fd.append('name', name);
|
|
139
|
+
return this.request('stickers', { method: 'POST', body: fd });
|
|
140
|
+
}
|
|
141
|
+
/** 删除贴图素材 */
|
|
142
|
+
async deleteSticker(id) {
|
|
143
|
+
await this.request(`stickers?id=${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
144
|
+
}
|
|
145
|
+
/** 版式模板清单(租户级资产):封面底图 + 固定描述({城市}{明星} 占位符) */
|
|
146
|
+
async posterTemplates() {
|
|
147
|
+
const r = await this.request('poster-templates');
|
|
148
|
+
return Array.isArray(r) ? r : [];
|
|
149
|
+
}
|
|
150
|
+
/** 上传版式模板(不压缩,保留封面底图原样) */
|
|
151
|
+
async createPosterTemplate(filePath, name, prompt) {
|
|
152
|
+
const blob = new Blob([new Uint8Array(readFileSync(filePath))]);
|
|
153
|
+
const fd = new FormData();
|
|
154
|
+
fd.append('file', blob, basename(filePath));
|
|
155
|
+
fd.append('name', name);
|
|
156
|
+
fd.append('prompt', prompt);
|
|
157
|
+
return this.request('poster-templates', { method: 'POST', body: fd });
|
|
158
|
+
}
|
|
159
|
+
/** 删除版式模板 */
|
|
160
|
+
async deletePosterTemplate(id) {
|
|
161
|
+
await this.request(`poster-templates?id=${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
162
|
+
}
|
|
128
163
|
/** 提交出图任务,立即返回 jobId */
|
|
129
164
|
async generate(opts) {
|
|
130
165
|
// prompt / skill_slug / template_id 三选一:都传时服务端按 prompt > template_id > skill_slug
|
|
@@ -310,9 +345,21 @@ export class StudioClient {
|
|
|
310
345
|
* 上传素材。图片会先压到视觉模型够用的尺寸再传(见 compress.ts)——
|
|
311
346
|
* 参考图是给模型看的,不是留档,原图直传只会拖慢上传和解析。
|
|
312
347
|
*/
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
348
|
+
/**
|
|
349
|
+
* 上传素材。默认只存文件、回直链(参考图/垫图就该这样)。
|
|
350
|
+
*
|
|
351
|
+
* asWork=true 时另外记一条作品:不落库的话文件只存在 R2 里,
|
|
352
|
+
* 作品页、后台画廊、项目归档全都看不见它 —— 「传到我的账户」就没发生。
|
|
353
|
+
* 只对账户身份生效(作品要归到具体某个人头上),租户 key 传了也会被中台忽略。
|
|
354
|
+
*/
|
|
355
|
+
async uploadRef(filePath, opts = {}) {
|
|
356
|
+
const form = await fileForm(filePath);
|
|
357
|
+
if (opts.asWork)
|
|
358
|
+
form.append('as_work', '1');
|
|
359
|
+
if (opts.workspaceId)
|
|
360
|
+
form.append('workspace_id', opts.workspaceId);
|
|
361
|
+
const r = await this.request('upload-ref', { method: 'POST', body: form });
|
|
362
|
+
return { url: r.url, media_type: r.media_type, mime: r.mime, job_id: r.job_id ?? null };
|
|
316
363
|
}
|
|
317
364
|
/**
|
|
318
365
|
* 图片转模板(POST /api/image-to-template):一张图 → 一个可复用的图片模板。
|
package/dist/commands/balance.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
export async function balance(client) {
|
|
2
2
|
const r = await client.balance();
|
|
3
|
-
// 单位 ¥ 人民币(后台 2026-08-09
|
|
4
|
-
|
|
3
|
+
// 单位 ¥ 人民币(后台 2026-08-09 起只返回租户自己的余额,不再下发上游供应商聚合数据)。
|
|
4
|
+
// 优先读 balance_cny:中台已改用这个名字,balance_usd 是双发过渡期的旧名,
|
|
5
|
+
// 两个值永远相等,但等中台停发旧名时这里不用再改一次。
|
|
6
|
+
const cny = r.balance_cny ?? r.balance_usd;
|
|
7
|
+
process.stderr.write(`余额: ¥${cny?.toFixed(2) ?? '?'}`);
|
|
5
8
|
if (r.markup_pct)
|
|
6
9
|
process.stderr.write(` 加价率: ${(r.markup_pct * 100).toFixed(0)}%`);
|
|
7
10
|
if (r.checked_at)
|
|
@@ -68,7 +68,8 @@ export async function compressCmd(input, opts) {
|
|
|
68
68
|
export async function removeBgCmd(input, opts) {
|
|
69
69
|
if (!(await fileExists(input)))
|
|
70
70
|
throw new Error(`文件不存在: ${input}`);
|
|
71
|
-
|
|
71
|
+
// 默认 birefnet:实测对毛发、白色主体、低对比度背景的召回远好于 isnet/u2net
|
|
72
|
+
const modelKey = (opts.model || 'birefnet');
|
|
72
73
|
if (!(modelKey in BG_MODELS))
|
|
73
74
|
throw new Error(`--model 只支持 ${Object.keys(BG_MODELS).join(' / ')}`);
|
|
74
75
|
const start = Date.now();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* museav poster-templates —— 版式模板库(租户级资产)。
|
|
3
|
+
* 封面底图 + 固定描述({城市} {明星} 占位符),选版式时把城市/明星名填进底图。
|
|
4
|
+
* 学员做的封面版式保存后全租户共享。
|
|
5
|
+
*/
|
|
6
|
+
import type { StudioClient } from '../client.js';
|
|
7
|
+
export declare function posterTemplates(client: StudioClient): Promise<void>;
|
|
8
|
+
export declare function createPosterTemplate(client: StudioClient, filePath: string, opts: {
|
|
9
|
+
name: string;
|
|
10
|
+
prompt: string;
|
|
11
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export async function posterTemplates(client) {
|
|
2
|
+
const list = await client.posterTemplates();
|
|
3
|
+
if (!list.length) {
|
|
4
|
+
process.stderr.write('没有版式模板(可用 museav poster-templates add <底图> --name <名称> --prompt <描述> 保存)\n');
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
process.stderr.write(`版式模板(${list.length} 个):\n`);
|
|
8
|
+
for (const t of list) {
|
|
9
|
+
process.stderr.write(` ${t.id.padEnd(38)} ${String(t.name || '').padEnd(18)} ${String(t.prompt || '').slice(0, 40)}\n`);
|
|
10
|
+
}
|
|
11
|
+
process.stderr.write(`\n保存: museav poster-templates add <底图路径> --name <名称> --prompt <描述>\n`);
|
|
12
|
+
console.log(list.map((t) => t.id).join('\n'));
|
|
13
|
+
}
|
|
14
|
+
export async function createPosterTemplate(client, filePath, opts) {
|
|
15
|
+
if (!opts.name?.trim())
|
|
16
|
+
throw new Error('--name 必填');
|
|
17
|
+
if (!opts.prompt?.trim())
|
|
18
|
+
throw new Error('--prompt 必填,{城市} {明星} 占位符会被替换');
|
|
19
|
+
const row = await client.createPosterTemplate(filePath, opts.name, opts.prompt);
|
|
20
|
+
const id = row?.template?.id || row?.id || '';
|
|
21
|
+
process.stderr.write(`✅ 版式模板已保存:${id}\n`);
|
|
22
|
+
console.log(id);
|
|
23
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface SpeakCliOptions {
|
|
2
|
+
out?: string;
|
|
3
|
+
voice?: string;
|
|
4
|
+
design?: string;
|
|
5
|
+
clone?: string;
|
|
6
|
+
instruction?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function speak(text: string, opts?: SpeakCliOptions): Promise<void>;
|
|
9
|
+
export declare function transcribeCmd(audioPath: string): Promise<void>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* museav speak —— 文本转语音,stdout 输出生成的文件路径。
|
|
3
|
+
* museav transcribe —— 语音转文本,stdout 输出识别结果。
|
|
4
|
+
*
|
|
5
|
+
* 三种音色来源,给了什么参数就走哪条:
|
|
6
|
+
* 默认 预置音色(--voice Chloe)
|
|
7
|
+
* --design 一句话描述音色,当场造一个
|
|
8
|
+
* --clone 拿一段音频当样本,克隆它的音色
|
|
9
|
+
*
|
|
10
|
+
* 直连小米 MiMo,不经中台,需要 MIMO_API_KEY —— 原因见 src/mimo-speech.ts 的文件头。
|
|
11
|
+
*/
|
|
12
|
+
import { writeFile } from 'node:fs/promises';
|
|
13
|
+
import { basename, resolve } from 'node:path';
|
|
14
|
+
import { synthesize, transcribe, speechMode, wavSeconds, DEFAULT_VOICE } from '../mimo-speech.js';
|
|
15
|
+
const MODE_LABEL = { tts: '预置音色', design: '音色设计', clone: '音色克隆' };
|
|
16
|
+
export async function speak(text, opts = {}) {
|
|
17
|
+
const mode = speechMode({ clonePath: opts.clone, design: opts.design });
|
|
18
|
+
// 克隆模式下 opts.clone 是整条路径,进度行里只留文件名——绝对路径会把这行顶到换行
|
|
19
|
+
const detail = mode === 'tts' ? (opts.voice || DEFAULT_VOICE)
|
|
20
|
+
: mode === 'clone' ? basename(opts.clone || '') : (opts.design || '');
|
|
21
|
+
process.stderr.write(`合成中(${MODE_LABEL[mode]}${detail ? ` · ${detail}` : ''})...\n`);
|
|
22
|
+
const buf = await synthesize(text, {
|
|
23
|
+
voice: opts.voice,
|
|
24
|
+
design: opts.design,
|
|
25
|
+
clonePath: opts.clone,
|
|
26
|
+
instruction: opts.instruction,
|
|
27
|
+
});
|
|
28
|
+
// 默认落在当前目录,文件名带时间戳避免连续合成互相覆盖
|
|
29
|
+
const out = resolve(opts.out || `speech-${Date.now()}.wav`);
|
|
30
|
+
await writeFile(out, buf);
|
|
31
|
+
const secs = wavSeconds(buf);
|
|
32
|
+
process.stderr.write(`✅ ${(buf.length / 1024).toFixed(0)}KB${secs ? ` · ${secs.toFixed(2)}s` : ''}\n`);
|
|
33
|
+
console.log(out);
|
|
34
|
+
}
|
|
35
|
+
export async function transcribeCmd(audioPath) {
|
|
36
|
+
process.stderr.write(`识别中 ${audioPath} ...\n`);
|
|
37
|
+
const text = await transcribe(audioPath);
|
|
38
|
+
// 质量有波动(见 mimo-speech.ts 的注释),提醒一句,但不影响 stdout 的机器可读性
|
|
39
|
+
process.stderr.write('✅ 识别完成(同音字可能有误,重要场景请核对)\n');
|
|
40
|
+
console.log(text);
|
|
41
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* museav stickers —— 贴图素材库(租户级资产)。
|
|
3
|
+
* PNG 透明装饰图(logo/花纹/边框),叠加在海报/封面上。
|
|
4
|
+
* 列出的都是自己租户的贴图;上传用账户 Key 或租户 Key 均可(落到本租户)。
|
|
5
|
+
*/
|
|
6
|
+
import type { StudioClient } from '../client.js';
|
|
7
|
+
export declare function stickers(client: StudioClient): Promise<void>;
|
|
8
|
+
export declare function createSticker(client: StudioClient, filePath: string, opts: {
|
|
9
|
+
name: string;
|
|
10
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export async function stickers(client) {
|
|
2
|
+
const list = await client.stickers();
|
|
3
|
+
if (!list.length) {
|
|
4
|
+
process.stderr.write('没有贴图素材(可用 museav stickers add <图片> --name <名称> 上传)\n');
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
process.stderr.write(`贴图素材(${list.length} 个):\n`);
|
|
8
|
+
for (const s of list) {
|
|
9
|
+
process.stderr.write(` ${s.id.padEnd(38)} ${String(s.name || '').padEnd(20)} ${s.url || ''}\n`);
|
|
10
|
+
}
|
|
11
|
+
process.stderr.write(`\n上传: museav stickers add <图片路径> --name <名称>\n`);
|
|
12
|
+
// stdout 只出 id,便于脚本与 agent 解析
|
|
13
|
+
console.log(list.map((s) => s.id).join('\n'));
|
|
14
|
+
}
|
|
15
|
+
export async function createSticker(client, filePath, opts) {
|
|
16
|
+
if (!opts.name?.trim())
|
|
17
|
+
throw new Error('--name 必填');
|
|
18
|
+
const row = await client.createSticker(filePath, opts.name);
|
|
19
|
+
const id = row?.sticker?.id || row?.id || '';
|
|
20
|
+
process.stderr.write(`✅ 贴图素材已上传:${id}\n`);
|
|
21
|
+
console.log(id);
|
|
22
|
+
}
|
|
@@ -6,4 +6,7 @@
|
|
|
6
6
|
* 拿到的 URL 可以直接喂给 gen --ref / gen --video --image,也能给 reverse 当图片 URL。
|
|
7
7
|
*/
|
|
8
8
|
import type { StudioClient } from '../client.js';
|
|
9
|
-
export declare function upload(client: StudioClient, filePath: string
|
|
9
|
+
export declare function upload(client: StudioClient, filePath: string, opts?: {
|
|
10
|
+
toWorks?: boolean;
|
|
11
|
+
workspace?: string;
|
|
12
|
+
}): Promise<void>;
|
package/dist/commands/upload.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
const KIND_LABEL = { image: '图片', audio: '音频', video: '视频' };
|
|
2
|
-
export async function upload(client, filePath) {
|
|
2
|
+
export async function upload(client, filePath, opts = {}) {
|
|
3
3
|
process.stderr.write(`上传 ${filePath} ...\n`);
|
|
4
|
-
const { url, media_type, mime } = await client.uploadRef(filePath
|
|
4
|
+
const { url, media_type, mime, job_id } = await client.uploadRef(filePath, {
|
|
5
|
+
asWork: opts.toWorks,
|
|
6
|
+
workspaceId: opts.workspace,
|
|
7
|
+
});
|
|
5
8
|
const kind = media_type ? `${KIND_LABEL[media_type] || media_type}${mime ? ` · ${mime}` : ''}` : '';
|
|
6
9
|
process.stderr.write(`✅ 上传成功${kind ? `(${kind})` : ''}\n`);
|
|
10
|
+
if (opts.toWorks) {
|
|
11
|
+
// 说清有没有真的进作品库:租户 key 调用时中台不记作品,只提示「已上传」会让人以为进去了
|
|
12
|
+
process.stderr.write(job_id
|
|
13
|
+
? '📁 已收进你的作品库,在「我的作品」里能看到\n'
|
|
14
|
+
: '⚠️ 文件已上传,但没能记进作品库(租户 Key 调用不记作品,作品要归到具体账户)\n');
|
|
15
|
+
}
|
|
16
|
+
else if (media_type === 'video') {
|
|
17
|
+
// 传视频十有八九是想收成品,顺手提一句 —— 但不擅自替他决定
|
|
18
|
+
process.stderr.write('提示:加 --to-works 可以把它收进「我的作品」\n');
|
|
19
|
+
}
|
|
7
20
|
console.log(url);
|
|
8
21
|
}
|
package/dist/compress.js
CHANGED
|
@@ -47,6 +47,14 @@ export async function compressForVision(filePath) {
|
|
|
47
47
|
meta = await sharp(filePath).metadata();
|
|
48
48
|
}
|
|
49
49
|
catch {
|
|
50
|
+
// HEIC 解码失败会走到这里(libheif 安全检查拒绝),但它不是「视频/音频」——
|
|
51
|
+
// 原样上传会被中台按魔数判成 image/heic,而 gpt-image-2 上游解码不了 HEIC
|
|
52
|
+
// 字节 → 400 invalid_image_input。与其等到出图时才炸,不如上传前就报清楚。
|
|
53
|
+
// 判断依据:文件名扩展名 .heic/.heif/.avif 最可靠(HEIC 与 MP4 共享 ftyp 容器头,
|
|
54
|
+
// 不能靠字节区分;sharp 读不了恰恰说明它是图不是视频——视频不喂 sharp 走这里)。
|
|
55
|
+
if (/\.(heic|heif|avif)$/i.test(name)) {
|
|
56
|
+
throw new Error(`HEIC 图片 ${name} 无法直接上传:上游模型认不得 HEIC 字节。请先转成 JPG/PNG 再上传(macOS 可用「预览」打开后另存为 JPEG,或用 museav img min 转换)`);
|
|
57
|
+
}
|
|
50
58
|
return { buffer: null, filename: name, note: '' }; // 不是 sharp 认识的图(视频/音频)→ 原样传
|
|
51
59
|
}
|
|
52
60
|
const longEdge = Math.max(meta.width || 0, meta.height || 0);
|
package/dist/index.js
CHANGED
|
@@ -28,6 +28,9 @@ import { jobs } from './commands/jobs.js';
|
|
|
28
28
|
import { whoami } from './commands/whoami.js';
|
|
29
29
|
import { products } from './commands/products.js';
|
|
30
30
|
import { assets } from './commands/assets.js';
|
|
31
|
+
import { stickers, createSticker } from './commands/stickers.js';
|
|
32
|
+
import { posterTemplates, createPosterTemplate } from './commands/poster-templates.js';
|
|
33
|
+
import { speak, transcribeCmd } from './commands/speak.js';
|
|
31
34
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
32
35
|
// 每 12 小时最多查一次 npm registry,过期才提示,不拖慢日常调用
|
|
33
36
|
updateNotifier({ pkg, updateCheckInterval: 1000 * 60 * 60 * 12 }).notify({ defer: false });
|
|
@@ -179,9 +182,9 @@ program
|
|
|
179
182
|
.action(asyncRun((input, opts) => compressCmd(input, opts)));
|
|
180
183
|
program
|
|
181
184
|
.command('remove-bg <file>')
|
|
182
|
-
.description('本地抠图去背景(ISNet/U2Net + onnxruntime,免登录):输出带 alpha 的 PNG
|
|
185
|
+
.description('本地抠图去背景(BiRefNet/ISNet/U2Net + onnxruntime,免登录):输出带 alpha 的 PNG。首次使用自动下载模型(缓存 ~/.museav-models)')
|
|
183
186
|
.option('--out <path>', '输出路径(默认 <名>-nobg.png)')
|
|
184
|
-
.option('--model <name>', 'isnet
|
|
187
|
+
.option('--model <name>', 'birefnet(默认,细节最好,~214MB)/ isnet / u2net')
|
|
185
188
|
.option('--overwrite', '允许覆盖已存在的输出文件')
|
|
186
189
|
.action(asyncRun((input, opts) => removeBgCmd(input, opts)));
|
|
187
190
|
program
|
|
@@ -247,7 +250,22 @@ program
|
|
|
247
250
|
program
|
|
248
251
|
.command('upload <file>')
|
|
249
252
|
.description('上传素材(图片/音频/视频,按字节内容判类型;图片 8MB / 音频 20MB / 视频 50MB),stdout 输出公网直链')
|
|
250
|
-
.
|
|
253
|
+
.option('--to-works', '同时收进「我的作品」(在外面做好的成品视频/图片用这个;参考图不用)')
|
|
254
|
+
.option('--workspace <id>', '归档到指定项目')
|
|
255
|
+
.action(withClient((client, file, opts) => upload(client, file, { toWorks: opts.toWorks, workspace: opts.workspace })));
|
|
256
|
+
program
|
|
257
|
+
.command('speak <text>')
|
|
258
|
+
.description('文本转语音(小米 MiMo,直连上游需 MIMO_API_KEY,不走中台身份):stdout 输出 wav 路径')
|
|
259
|
+
.option('--out <path>', '输出路径(默认 speech-<时间戳>.wav)')
|
|
260
|
+
.option('--voice <name>', '预置音色,默认 Chloe')
|
|
261
|
+
.option('--design <desc>', '一句话描述音色,当场造一个(如「低沉沙哑的中年男声」)')
|
|
262
|
+
.option('--clone <file>', '拿这段音频当样本,克隆它的音色')
|
|
263
|
+
.option('--instruction <text>', '语气/风格指令(三种模式都可用)')
|
|
264
|
+
.action(asyncRun((text, opts) => speak(text, opts)));
|
|
265
|
+
program
|
|
266
|
+
.command('transcribe <audio>')
|
|
267
|
+
.description('语音转文本(小米 MiMo,需 MIMO_API_KEY):stdout 输出识别结果。同音字可能有误,重要场景请核对')
|
|
268
|
+
.action(asyncRun((audio) => transcribeCmd(audio)));
|
|
251
269
|
program
|
|
252
270
|
.command('models')
|
|
253
271
|
.description('查可用模型列表')
|
|
@@ -299,6 +317,25 @@ templatesCmd
|
|
|
299
317
|
.option('--fields <json>', '占位符字段说明,JSON 数组,如 \'[{"key":"artist","label":"艺人名"}]\';不传则自动从 --prompt 里的 {key} 提取')
|
|
300
318
|
.option('--type <type>', '模板类型:image(图片,默认) / article(文字)')
|
|
301
319
|
.action(withClient((client, opts) => createTemplate(client, opts)));
|
|
320
|
+
const stickersCmd = program
|
|
321
|
+
.command('stickers')
|
|
322
|
+
.description('查贴图素材库(租户级资产:PNG 装饰图,叠加在海报/封面上)')
|
|
323
|
+
.action(withClient((client) => stickers(client)));
|
|
324
|
+
stickersCmd
|
|
325
|
+
.command('add <file>')
|
|
326
|
+
.description('上传贴图素材(PNG 透明装饰图,不压缩保留透明通道)')
|
|
327
|
+
.requiredOption('--name <名称>', '贴图名称')
|
|
328
|
+
.action(withClient((client, file, opts) => createSticker(client, file, opts)));
|
|
329
|
+
const posterTemplatesCmd = program
|
|
330
|
+
.command('poster-templates')
|
|
331
|
+
.description('查版式模板库(租户级资产:封面底图 + 固定描述,选版式把城市/明星名填进底图)')
|
|
332
|
+
.action(withClient((client) => posterTemplates(client)));
|
|
333
|
+
posterTemplatesCmd
|
|
334
|
+
.command('add <file>')
|
|
335
|
+
.description('保存版式模板(封面底图 + 描述,描述里用 {城市} {明星} 占位)')
|
|
336
|
+
.requiredOption('--name <名称>', '版式名称')
|
|
337
|
+
.requiredOption('--prompt <描述>', '版式固定描述,{城市} {明星} 会自动替换')
|
|
338
|
+
.action(withClient((client, file, opts) => createPosterTemplate(client, file, opts)));
|
|
302
339
|
program
|
|
303
340
|
.command('products')
|
|
304
341
|
.description('查所属租户自己的产品目录(数据在租户自己的后台,不在 Studio 中台;只支持已开通该接口的租户,仅租户 apiKey 身份可用)')
|
package/dist/local-bg.d.ts
CHANGED
|
@@ -1,13 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 每个模型的输入尺寸与归一化参数**都不一样**,必须随模型带着走。
|
|
3
|
+
* 取值对齐 rembg 的 session 定义(sessions/dis_general_use.py、sessions/u2net.py):
|
|
4
|
+
* isnet-general-use → resize 1024,mean (0.5,0.5,0.5) std (1,1,1)
|
|
5
|
+
* u2net → resize 320,mean ImageNet std ImageNet
|
|
6
|
+
* 用错任何一项都不会报错,只会安静地输出一张糊掉的 mask —— 这是最难查的那种 bug。
|
|
7
|
+
*/
|
|
1
8
|
export declare const BG_MODELS: {
|
|
2
9
|
readonly isnet: {
|
|
3
10
|
readonly file: 'isnet-general-use.onnx';
|
|
4
11
|
readonly url: 'https://github.com/danielgatis/rembg/releases/download/v0.0.0/isnet-general-use.onnx';
|
|
5
12
|
readonly label: 'ISNet(通用,质量优先)';
|
|
13
|
+
readonly edge: 1024;
|
|
14
|
+
readonly mean: readonly [0.5, 0.5, 0.5];
|
|
15
|
+
readonly std: readonly [1, 1, 1];
|
|
6
16
|
};
|
|
7
17
|
readonly u2net: {
|
|
8
18
|
readonly file: 'u2net.onnx';
|
|
9
19
|
readonly url: 'https://github.com/danielgatis/rembg/releases/download/v0.0.0/u2net.onnx';
|
|
10
20
|
readonly label: 'U2Net(经典通用)';
|
|
21
|
+
readonly edge: 320;
|
|
22
|
+
readonly mean: readonly [0.485, 0.456, 0.406];
|
|
23
|
+
readonly std: readonly [0.229, 0.224, 0.225];
|
|
24
|
+
};
|
|
25
|
+
readonly birefnet: {
|
|
26
|
+
readonly file: 'birefnet-general-lite.onnx';
|
|
27
|
+
readonly url: 'https://github.com/danielgatis/rembg/releases/download/v0.0.0/BiRefNet-general-bb_swin_v1_tiny-epoch_232.onnx';
|
|
28
|
+
readonly label: 'BiRefNet-Lite(细节最好,毛发/低对比度主体优先用它)';
|
|
29
|
+
readonly edge: 1024;
|
|
30
|
+
readonly mean: readonly [0.485, 0.456, 0.406];
|
|
31
|
+
readonly std: readonly [0.229, 0.224, 0.225];
|
|
32
|
+
readonly sigmoid: true;
|
|
33
|
+
readonly size: 214;
|
|
11
34
|
};
|
|
12
35
|
};
|
|
13
36
|
export type BgModelKey = keyof typeof BG_MODELS;
|