@pippit-dev/cli 1.0.32 → 1.0.33
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/README.md +19 -8
- package/checksums.txt +6 -6
- package/cmd/generate_image/generate_image.go +4 -2
- package/cmd/generate_image_test.go +150 -1
- package/cmd/model.go +7 -10
- package/cmd/model_test.go +59 -1
- package/cmd/short_drama_test.go +2 -0
- package/cmd/source_test.go +6 -1
- package/dist/checksums.txt +6 -6
- package/internal/common/ratio.go +12 -0
- package/internal/generate_image/generate_image.go +19 -7
- package/internal/generate_image/generate_image_test.go +57 -14
- package/internal/models/describe.go +79 -19
- package/internal/models/describe_test.go +131 -5
- package/internal/models/models.go +124 -22
- package/internal/models/models_test.go +158 -16
- package/package.json +1 -1
- package/skills/xyq-nest-skill/SKILL.md +2 -3
- package/skills/xyq-nest-skill/commands/generate-image.md +12 -7
- package/skills/xyq-nest-skill/commands/model.md +49 -7
- package/skills/xyq-nest-skill/examples/generate-and-deliver.md +4 -2
- package/skills/xyq-nest-skill/examples/image-edit.md +5 -3
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ node skills/xyq-marketing-skill/scripts/marketing.js query --thread-id THREAD_ID
|
|
|
51
51
|
| 登录授权 | `status` / `login` / `logout` | [授权](skills/xyq-nest-skill/commands/auth.md) |
|
|
52
52
|
| 个人 Canvas 画布与节点编辑 | `canvas` | [画布](skills/xyq-nest-skill/commands/canvas.md) |
|
|
53
53
|
| 生图、参考图编辑 | `generate-image` | [图片](skills/xyq-nest-skill/commands/generate-image.md) |
|
|
54
|
-
|
|
|
54
|
+
| 查看可用图片/视频模型、参数配置 | `model list` / `model describe` | [模型发现](skills/xyq-nest-skill/commands/model.md) |
|
|
55
55
|
| 生视频、首尾帧 | `generate-video` | [视频](skills/xyq-nest-skill/commands/generate-video.md) |
|
|
56
56
|
| 视频超分 | `video-super-resolution` | [超分](skills/xyq-nest-skill/commands/video-super-resolution.md) |
|
|
57
57
|
| 擦字幕 | `erase-video-subtitle` | [擦字幕](skills/xyq-nest-skill/commands/erase-video-subtitle.md) |
|
|
@@ -205,20 +205,22 @@ pippit-tool-cli canvas command run xyq.generation.update_prompt \
|
|
|
205
205
|
|
|
206
206
|
## 生图 CLI
|
|
207
207
|
|
|
208
|
-
`generate-image`
|
|
208
|
+
`generate-image` 使用图片模型的展示名称;先查询当前列表并选择模型,再上传本地参考图片并提交生图请求。以下假设列表包含该名称:
|
|
209
209
|
|
|
210
210
|
```bash
|
|
211
211
|
pippit-tool-cli generate-image \
|
|
212
212
|
--prompt "生成一张小猫海报" \
|
|
213
213
|
--image "~/images/cat.png" \
|
|
214
|
-
--model "
|
|
214
|
+
--model "智能图片V2.5 Fast" \
|
|
215
215
|
--ratio 6 \
|
|
216
216
|
--generate-image-count 2
|
|
217
217
|
```
|
|
218
218
|
|
|
219
|
-
命令输出 `thread_id`、`run_id` 和 `web_thread_link`。提交 HTTP 请求时,`agent_name` 固定为 `pippit_nest_agent`,参考图会使用上传接口返回的 `pippit_asset_id` 写入顶层 `asset_ids`,生图模型写入 `general_agent_settings.image_model`,比例写入 `general_agent_settings.ratio`,生图数量写入 `general_agent_settings.generate_image_count`。`--model`
|
|
219
|
+
命令输出 `thread_id`、`run_id` 和 `web_thread_link`。提交 HTTP 请求时,`agent_name` 固定为 `pippit_nest_agent`,参考图会使用上传接口返回的 `pippit_asset_id` 写入顶层 `asset_ids`,生图模型写入 `general_agent_settings.image_model`,比例写入 `general_agent_settings.ratio`,生图数量写入 `general_agent_settings.generate_image_count`。`--model` 为必填参数,填写 `model list --type image` 返回的完整名称,例如 `"智能图片V2.5 Fast"`。CLI 在上传素材前查询或复用有效缓存,解析名称后仅在提交请求中填写服务端模型标识;名称缺失、重复或未找到时停止,不猜测模型。
|
|
220
220
|
|
|
221
|
-
|
|
221
|
+
图片 `model list/search/describe` 返回给宿主的是 API 下发的展示名称 `name`,例如 `美学模型 8.2`。生成时填写 `--model "美学模型 8.2"`,CLI 仅在提交请求中使用该条目对应的 `key`。名称、可用模型和参数均由接口动态提供,不维护静态名称映射。
|
|
222
|
+
|
|
223
|
+
`--ratio` 可选,只接受服务端 `Ratio` 数字枚举,例如 `--ratio 3` 表示 `9:16`。通过 `model describe "模型名称" --type image` 查看当前可用的数字 `options/default`,`option_labels` 说明每个数字对应的比例。CLI 校验整数格式,模型是否支持该枚举由服务端决定。常用枚举值含义如下:
|
|
222
224
|
|
|
223
225
|
| ratio 参数 | IDL 枚举 | 含义 |
|
|
224
226
|
| ---: | --- | --- |
|
|
@@ -232,18 +234,27 @@ pippit-tool-cli generate-image \
|
|
|
232
234
|
|
|
233
235
|
`--generate-image-count` 可选,填写生图数量,对应 IDL 字段 `GeneralSettingsPart.GenerateImageCount` / JSON 字段 `generate_image_count`。CLI 只校验不能为负数;具体数量范围由服务端决定。
|
|
234
236
|
|
|
235
|
-
|
|
237
|
+
`--resolution` 写入 `general_agent_settings.resolution`(转大写),新增 `--effort` 写入 `general_agent_settings.image_effort`(转小写)。分辨率和推理强度选项都来自该模型的动态配置;没有 `effort` 维度的模型不展示推理强度选择。只传用户指定的参数,未指定时省略,不自动补查询默认值。
|
|
238
|
+
|
|
239
|
+
图片支持 `.jpg`、`.jpeg`、`.png`、`.gif`、`.bmp`、`.webp`、`.svg`。CLI 会在提交前校验 prompt、model 必填、ratio 格式、generate-image-count 非负和文件后缀;不新增模型或参数组合白名单。
|
|
236
240
|
|
|
237
|
-
##
|
|
241
|
+
## 图片与视频模型发现
|
|
238
242
|
|
|
239
243
|
```bash
|
|
244
|
+
# 图片模型及可选参数
|
|
245
|
+
pippit-tool-cli model list --type image
|
|
246
|
+
pippit-tool-cli model search "智能图片" --type image
|
|
247
|
+
pippit-tool-cli model describe "智能图片V2.5 Fast" --type image
|
|
248
|
+
pippit-tool-cli model list --type image --refresh
|
|
249
|
+
|
|
250
|
+
# 省略 --type 保持查询视频
|
|
240
251
|
pippit-tool-cli model list
|
|
241
252
|
pippit-tool-cli model search MiniMax
|
|
242
253
|
pippit-tool-cli model describe MiniMax-H3
|
|
243
254
|
pippit-tool-cli model list --refresh
|
|
244
255
|
```
|
|
245
256
|
|
|
246
|
-
|
|
257
|
+
使用当前个人登录凭证查询服务端 Skill 模型接口;图片 scene 为 `web_image_agent`,视频为 `web_turbo_video_generator`,不传 TeamID。成功结果按账号、环境和场景隔离缓存 5 分钟,`--refresh` 强制刷新。失败时提示重试,不回退静态列表或过期缓存。`describe` 提供可直接传给生成命令的参数:图片比例保留数字枚举并附比例说明,视频比例转换为字符串,同时整理分辨率、图片推理强度、视频时长和素材限制;不展示内部 `config_key`,未知比例枚举跳过。图片列表和详情仅用 `name` 标识模型,不展示底层模型枚举;搜索、详情查询和 `generate-image --model` 均使用名称,包含空格时加引号。图片详情保留原始 `parameter_config`,包括条件、必选标记、选项说明和参数组合约束。生成仍由服务端校验;服务端尚未开放图片场景或提交参数时会返回错误,不回退静态模型清单。详见 [模型发现](skills/xyq-nest-skill/commands/model.md)。
|
|
247
258
|
|
|
248
259
|
## 生视频 CLI
|
|
249
260
|
|
package/checksums.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
dd742dedf06e2645d02bc13a4cfa1af45778ae0200a8a6e67df177ba7e069775 pippit-tool-cli-1.0.33-darwin-amd64.tar.gz
|
|
2
|
+
8d9fc0112650839c430e58cb7d6917e5bb1b95fde49659aecbbf14d1421c28f5 pippit-tool-cli-1.0.33-darwin-arm64.tar.gz
|
|
3
|
+
f2f2f741c920b115b2a1a9c4c4f074c1a07fd6cbc3a442cf4dc4d793c1b9ef5a pippit-tool-cli-1.0.33-linux-amd64.tar.gz
|
|
4
|
+
2ed6e791c7dbea0e2ea8a1d05a0db298442218ecfc17ec47d70d49639f1d91b6 pippit-tool-cli-1.0.33-linux-arm64.tar.gz
|
|
5
|
+
f713c05325043f177f4a9e01a433b6a7fc8f57fea123f712ee459707955d199e pippit-tool-cli-1.0.33-windows-amd64.zip
|
|
6
|
+
7e637f67fbbb818db0e66c49f7814d7e79cd71857d8c3bd6a5397af9d18533ce pippit-tool-cli-1.0.33-windows-arm64.zip
|
|
@@ -34,6 +34,7 @@ func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command
|
|
|
34
34
|
"model": strings.TrimSpace(opts.Model),
|
|
35
35
|
"ratio": strings.TrimSpace(opts.Ratio),
|
|
36
36
|
"resolution": strings.ToUpper(strings.TrimSpace(opts.Resolution)),
|
|
37
|
+
"effort": strings.ToLower(strings.TrimSpace(opts.Effort)),
|
|
37
38
|
"generate_image_count": optionalIntString(opts.GenerateImageCount),
|
|
38
39
|
})
|
|
39
40
|
return err
|
|
@@ -47,9 +48,10 @@ func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command
|
|
|
47
48
|
flags.StringVar(&opts.Source, "source", "", "optional host agent/platform identifier for statistics only; filled silently by the host agent (e.g. doubao_office, workbuddy, codex)")
|
|
48
49
|
flags.StringVar(&opts.Prompt, "prompt", "", "image generation prompt")
|
|
49
50
|
flags.StringArrayVar(&opts.ImagePaths, "image", nil, "local reference image path; repeat for multiple images")
|
|
50
|
-
flags.StringVar(&opts.Model, "model", "", "image model
|
|
51
|
+
flags.StringVar(&opts.Model, "model", "", "exact image model name from 'model list --type image' (quote names containing spaces)")
|
|
51
52
|
flags.StringVar(&opts.Ratio, "ratio", "", "image ratio; "+internalgen.SupportedRatioUsage())
|
|
52
|
-
flags.StringVar(&opts.Resolution, "resolution", "", "image resolution;
|
|
53
|
+
flags.StringVar(&opts.Resolution, "resolution", "", "image resolution; use 'model describe \"MODEL_NAME\" --type image' for available values")
|
|
54
|
+
flags.StringVar(&opts.Effort, "effort", "", "image reasoning effort; only use values exposed by 'model describe \"MODEL_NAME\" --type image'")
|
|
53
55
|
flags.IntVar(&generateImageCount, "generate-image-count", 0, "generated image count")
|
|
54
56
|
return cmd
|
|
55
57
|
}
|
|
@@ -2,6 +2,7 @@ package cmd
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"bytes"
|
|
5
|
+
"encoding/json"
|
|
5
6
|
"io"
|
|
6
7
|
"net/http"
|
|
7
8
|
"net/http/httptest"
|
|
@@ -10,18 +11,29 @@ import (
|
|
|
10
11
|
"strings"
|
|
11
12
|
"testing"
|
|
12
13
|
|
|
14
|
+
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
13
15
|
"github.com/bytedance/sonic"
|
|
14
16
|
)
|
|
15
17
|
|
|
16
18
|
func TestGenerateImage(t *testing.T) {
|
|
19
|
+
t.Setenv("HOME", t.TempDir())
|
|
20
|
+
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
|
21
|
+
t.Setenv("LocalAppData", t.TempDir())
|
|
17
22
|
var uploaded bool
|
|
23
|
+
var modelQueried bool
|
|
18
24
|
|
|
19
25
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
20
26
|
if r.Header.Get("Authorization") != "Bearer test-token" {
|
|
21
27
|
t.Fatalf("Authorization = %q, want test bearer token", r.Header.Get("Authorization"))
|
|
22
28
|
}
|
|
23
29
|
switch r.URL.Path {
|
|
30
|
+
case "/api/biz/v1/skill/get_available_model_list":
|
|
31
|
+
modelQueried = true
|
|
32
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"scene":"web_image_agent","config_key":"image-key","config":{"models":[{"key":"seedream_5.0_pro","name":"Seedream 5.0 Pro","kind":"image"}]}}}`))
|
|
24
33
|
case "/api/biz/v1/skill/upload_file":
|
|
34
|
+
if !modelQueried {
|
|
35
|
+
t.Fatal("image name must be resolved before uploading")
|
|
36
|
+
}
|
|
25
37
|
if r.Method != http.MethodPost {
|
|
26
38
|
t.Fatalf("upload method = %s, want POST", r.Method)
|
|
27
39
|
}
|
|
@@ -100,7 +112,7 @@ func TestGenerateImage(t *testing.T) {
|
|
|
100
112
|
"generate-image",
|
|
101
113
|
"--prompt", "生成小猫海报",
|
|
102
114
|
"--image", image,
|
|
103
|
-
"--model", "
|
|
115
|
+
"--model", "Seedream 5.0 Pro",
|
|
104
116
|
"--ratio", "6",
|
|
105
117
|
"--resolution", "4K",
|
|
106
118
|
"--generate-image-count", "2",
|
|
@@ -137,3 +149,140 @@ func TestGenerateImageRequiresModel(t *testing.T) {
|
|
|
137
149
|
t.Fatalf("error = %q, want model validation", err)
|
|
138
150
|
}
|
|
139
151
|
}
|
|
152
|
+
|
|
153
|
+
func TestGenerateImageDynamicParameters(t *testing.T) {
|
|
154
|
+
t.Setenv("HOME", t.TempDir())
|
|
155
|
+
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
|
156
|
+
t.Setenv("LocalAppData", t.TempDir())
|
|
157
|
+
requests := 0
|
|
158
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
159
|
+
requests++
|
|
160
|
+
if r.URL.Path == "/api/biz/v1/skill/get_available_model_list" {
|
|
161
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"scene":"web_image_agent","config_key":"image-key","config":{"models":[{"key":"future-image-model","name":"未来图片模型","kind":"image"}]}}}`))
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
if r.URL.Path != "/api/biz/v1/skill/submit_run" {
|
|
165
|
+
t.Errorf("unexpected path: %s", r.URL.Path)
|
|
166
|
+
}
|
|
167
|
+
raw, err := io.ReadAll(r.Body)
|
|
168
|
+
if err != nil {
|
|
169
|
+
t.Error(err)
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
var body struct {
|
|
173
|
+
AgentName string `json:"agent_name"`
|
|
174
|
+
Settings struct {
|
|
175
|
+
Model string `json:"image_model"`
|
|
176
|
+
Ratio int `json:"ratio"`
|
|
177
|
+
Resolution string `json:"resolution"`
|
|
178
|
+
Effort string `json:"image_effort"`
|
|
179
|
+
} `json:"general_agent_settings"`
|
|
180
|
+
}
|
|
181
|
+
if err := sonic.Unmarshal(raw, &body); err != nil {
|
|
182
|
+
t.Error(err)
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
if body.AgentName != "pippit_nest_agent" || body.Settings.Model != "future-image-model" || body.Settings.Ratio != 13 || body.Settings.Resolution != "8K" || body.Settings.Effort != "future-effort" {
|
|
186
|
+
t.Errorf("incorrect request mapping: %s", raw)
|
|
187
|
+
}
|
|
188
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"run":{"thread_id":"image-thread","run_id":"image-run"}}}`))
|
|
189
|
+
}))
|
|
190
|
+
defer server.Close()
|
|
191
|
+
var stdout, stderr bytes.Buffer
|
|
192
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
193
|
+
root.SetArgs([]string{"generate-image", "--prompt", "image", "--model", "未来图片模型", "--ratio", "13", "--resolution", "8k", "--effort", " FUTURE-EFFORT "})
|
|
194
|
+
if err := root.Execute(); err != nil || requests != 2 {
|
|
195
|
+
t.Fatalf("Execute: err=%v requests=%d", err, requests)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
func TestImageModelNameFromDiscoveryToSubmission(t *testing.T) {
|
|
200
|
+
t.Setenv("HOME", t.TempDir())
|
|
201
|
+
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
|
202
|
+
t.Setenv("LocalAppData", t.TempDir())
|
|
203
|
+
queries, submits := 0, 0
|
|
204
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
205
|
+
switch r.URL.Path {
|
|
206
|
+
case config.GetAvailableModelListPath:
|
|
207
|
+
queries++
|
|
208
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"scene":"web_image_agent","config_key":"image-key","config":{"models":[{"key":"ali_midjourney_8_2","name":"美学模型 8.2","benefit_map_by_combination":{"ali_midjourney_8_2":{"resource_id":"ali_midjourney_8_2"}},"kind":"image"}]}}}`))
|
|
209
|
+
case config.SubmitRunPath:
|
|
210
|
+
submits++
|
|
211
|
+
var body struct {
|
|
212
|
+
Settings map[string]json.RawMessage `json:"general_agent_settings"`
|
|
213
|
+
}
|
|
214
|
+
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
215
|
+
t.Error(err)
|
|
216
|
+
}
|
|
217
|
+
if string(body.Settings["image_model"]) != `"ali_midjourney_8_2"` || len(body.Settings) != 1 {
|
|
218
|
+
t.Errorf("wire model not resolved or unrequested defaults filled: %v", body.Settings)
|
|
219
|
+
}
|
|
220
|
+
_, _ = w.Write([]byte(`{"ret":"1","errmsg":"ali_midjourney_8_2 暂时不可用","log_id":"image-log"}`))
|
|
221
|
+
default:
|
|
222
|
+
t.Errorf("unexpected request: %s", r.URL.Path)
|
|
223
|
+
}
|
|
224
|
+
}))
|
|
225
|
+
defer server.Close()
|
|
226
|
+
execute := func(args ...string) (string, string, error) {
|
|
227
|
+
var stdout, stderr bytes.Buffer
|
|
228
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
229
|
+
root.SetArgs(args)
|
|
230
|
+
err := root.Execute()
|
|
231
|
+
return stdout.String(), stderr.String(), err
|
|
232
|
+
}
|
|
233
|
+
list, _, err := execute("model", "list", "--type", "image")
|
|
234
|
+
if err != nil {
|
|
235
|
+
t.Fatal(err)
|
|
236
|
+
}
|
|
237
|
+
var output struct{ Models []struct{ Name string } }
|
|
238
|
+
if err := json.Unmarshal([]byte(list), &output); err != nil || len(output.Models) != 1 {
|
|
239
|
+
t.Fatalf("list=%s err=%v", list, err)
|
|
240
|
+
}
|
|
241
|
+
name := output.Models[0].Name
|
|
242
|
+
if name != "美学模型 8.2" {
|
|
243
|
+
t.Fatalf("unexpected display name: %q", name)
|
|
244
|
+
}
|
|
245
|
+
detail, _, err := execute("model", "describe", name, "--type", "image")
|
|
246
|
+
if err != nil || !strings.Contains(detail, name) || strings.Contains(list+detail, "ali_midjourney_8_2") {
|
|
247
|
+
t.Fatalf("user-facing identity changed: list=%s detail=%s err=%v", list, detail, err)
|
|
248
|
+
}
|
|
249
|
+
stdout, stderr, err := execute("generate-image", "--prompt", "一张猫咪图片", "--model", name)
|
|
250
|
+
if err == nil || !strings.Contains(err.Error(), name) || !strings.Contains(err.Error(), "image-log") || strings.Contains(stdout+stderr+err.Error(), "ali_midjourney_8_2") {
|
|
251
|
+
t.Fatalf("server rejection must keep name and log ID: stdout=%s stderr=%s err=%v", stdout, stderr, err)
|
|
252
|
+
}
|
|
253
|
+
if queries != 1 || submits != 1 {
|
|
254
|
+
t.Fatalf("expected shared cache and no submit retry: queries=%d submits=%d", queries, submits)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
func TestImageNameFailureStopsBeforeUploadAndSubmit(t *testing.T) {
|
|
259
|
+
for _, tc := range []struct{ name, selection, models string }{
|
|
260
|
+
{"unknown name", "不存在", `[{"key":"private-image","name":"图片模型","kind":"image"}]`},
|
|
261
|
+
{"wire key", "private-image", `[{"key":"private-image","name":"图片模型","kind":"image"}]`},
|
|
262
|
+
{"missing name", "图片模型", `[{"key":"private-image","kind":"image"}]`},
|
|
263
|
+
{"duplicate name", "图片模型", `[{"key":"private-image","name":"图片模型","kind":"image"},{"key":"private-image-2","name":"图片模型","kind":"image"}]`},
|
|
264
|
+
{"empty list", "图片模型", `[]`},
|
|
265
|
+
} {
|
|
266
|
+
t.Run(tc.name, func(t *testing.T) {
|
|
267
|
+
t.Setenv("HOME", t.TempDir())
|
|
268
|
+
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
|
269
|
+
t.Setenv("LocalAppData", t.TempDir())
|
|
270
|
+
requests := 0
|
|
271
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
272
|
+
requests++
|
|
273
|
+
if r.URL.Path != config.GetAvailableModelListPath {
|
|
274
|
+
t.Errorf("must not upload or submit: %s", r.URL.Path)
|
|
275
|
+
}
|
|
276
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"scene":"web_image_agent","config_key":"image-key","config":{"models":` + tc.models + `}}}`))
|
|
277
|
+
}))
|
|
278
|
+
defer server.Close()
|
|
279
|
+
var stdout, stderr bytes.Buffer
|
|
280
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
281
|
+
root.SetArgs([]string{"generate-image", "--prompt", "image", "--model", tc.selection, "--image", "/missing.png"})
|
|
282
|
+
err := root.Execute()
|
|
283
|
+
if err == nil || requests != 1 || strings.Contains(stdout.String()+stderr.String()+err.Error(), "private-image") {
|
|
284
|
+
t.Fatalf("invalid name accepted or exposed: requests=%d stdout=%s stderr=%s err=%v", requests, stdout.String(), stderr.String(), err)
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
}
|
package/cmd/model.go
CHANGED
|
@@ -16,10 +16,7 @@ func newModelCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Com
|
|
|
16
16
|
var modelType string
|
|
17
17
|
service := models.NewService(runner)
|
|
18
18
|
query := func(cmd *cobra.Command, args []string, describe bool) error {
|
|
19
|
-
|
|
20
|
-
return fmt.Errorf("当前仅支持 --type video")
|
|
21
|
-
}
|
|
22
|
-
result, err := service.Get(cmd.Context(), refresh)
|
|
19
|
+
result, err := service.Get(cmd.Context(), modelType, refresh)
|
|
23
20
|
if err != nil {
|
|
24
21
|
return err
|
|
25
22
|
}
|
|
@@ -43,9 +40,9 @@ func newModelCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Com
|
|
|
43
40
|
return common.WriteJSON(stdout, output)
|
|
44
41
|
}
|
|
45
42
|
cmd := &cobra.Command{
|
|
46
|
-
Use: "model [key]",
|
|
47
|
-
Short: "Discover available video models and their server configuration",
|
|
48
|
-
Long: "Query available video models using your current credentials. Successful queries are cached locally for 5 minutes. Use --refresh to bypass the cache; retry if a query fails.",
|
|
43
|
+
Use: "model [name-or-key]",
|
|
44
|
+
Short: "Discover available image and video models and their server configuration",
|
|
45
|
+
Long: "Query available image and video models using your current credentials. Select images by their full display name (in quotes), videos by key. Successful queries are cached locally for 5 minutes. Use --refresh to bypass the cache; retry if a query fails.",
|
|
49
46
|
Args: cobra.MaximumNArgs(1),
|
|
50
47
|
RunE: withErrorLog("model", nil, func(cmd *cobra.Command, args []string) error {
|
|
51
48
|
return query(cmd, args, len(args) > 0)
|
|
@@ -54,17 +51,17 @@ func newModelCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Com
|
|
|
54
51
|
cmd.SetOut(stdout)
|
|
55
52
|
cmd.SetErr(stderr)
|
|
56
53
|
cmd.PersistentFlags().BoolVar(&refresh, "refresh", false, "refresh the 5-minute local model cache")
|
|
57
|
-
cmd.PersistentFlags().StringVarP(&modelType, "type", "t", "video", "model type (
|
|
54
|
+
cmd.PersistentFlags().StringVarP(&modelType, "type", "t", "video", "model type: video or image (default video)")
|
|
58
55
|
cmd.AddCommand(&cobra.Command{
|
|
59
56
|
Use: "list [query]", Aliases: []string{"search"},
|
|
60
|
-
Short: "List
|
|
57
|
+
Short: "List models; search images by name, videos by key or name",
|
|
61
58
|
Args: cobra.MaximumNArgs(1),
|
|
62
59
|
RunE: withErrorLog("model list", nil, func(cmd *cobra.Command, args []string) error {
|
|
63
60
|
return query(cmd, args, false)
|
|
64
61
|
}),
|
|
65
62
|
})
|
|
66
63
|
cmd.AddCommand(&cobra.Command{
|
|
67
|
-
Use: "describe <key>", Short: "Show model parameters
|
|
64
|
+
Use: "describe <name-or-key>", Short: "Show model parameters; use the full image name or video key",
|
|
68
65
|
Args: cobra.ExactArgs(1),
|
|
69
66
|
RunE: withErrorLog("model describe", nil, func(cmd *cobra.Command, args []string) error {
|
|
70
67
|
return query(cmd, args, true)
|
package/cmd/model_test.go
CHANGED
|
@@ -67,7 +67,7 @@ func TestModelCommands(t *testing.T) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
func TestModelHelpDoesNotRequireLoginOrRequest(t *testing.T) {
|
|
70
|
-
for _, args := range [][]string{{"model", "--help"}, {"generate-video", "--help"}} {
|
|
70
|
+
for _, args := range [][]string{{"model", "--help"}, {"generate-video", "--help"}, {"generate-image", "--help"}} {
|
|
71
71
|
var stdout, stderr bytes.Buffer
|
|
72
72
|
root := NewRootCommand(&stdout, &stderr)
|
|
73
73
|
root.SetArgs(args)
|
|
@@ -79,3 +79,61 @@ func TestModelHelpDoesNotRequireLoginOrRequest(t *testing.T) {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
+
|
|
83
|
+
func TestImageModelCommands(t *testing.T) {
|
|
84
|
+
t.Setenv("HOME", t.TempDir())
|
|
85
|
+
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
|
86
|
+
t.Setenv("LocalAppData", t.TempDir())
|
|
87
|
+
requests := 0
|
|
88
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
89
|
+
requests++
|
|
90
|
+
var body map[string]string
|
|
91
|
+
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body["scene"] != "web_image_agent" || body["source_model_key"] != "" || r.URL.Path != config.GetAvailableModelListPath {
|
|
92
|
+
t.Errorf("unexpected request: body=%v path=%s err=%v", body, r.URL.Path, err)
|
|
93
|
+
}
|
|
94
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"scene":"web_image_agent","config_key":"image-config","config":{"models":[{"key":"image-model","report_name":"image-model","name":"图片测试模型","kind":"image","is_default":true,"supported_ratio_list":[0,2,6],"parameter_config":{"dimensions":[{"key":"resolution","default_value":"2K","option_list":[{"value":"2K"}]},{"key":"effort","default_value":"low","option_list":[{"value":"low"},{"value":"high"}]}]}}]}}}`))
|
|
95
|
+
}))
|
|
96
|
+
defer server.Close()
|
|
97
|
+
for _, args := range [][]string{
|
|
98
|
+
{"model", "list", "--type", "image"},
|
|
99
|
+
{"model", "search", "图片", "-t", "image"},
|
|
100
|
+
{"model", "describe", "图片测试模型", "--type", "image"},
|
|
101
|
+
{"model", "图片测试模型", "-t", "image"},
|
|
102
|
+
} {
|
|
103
|
+
var stdout, stderr bytes.Buffer
|
|
104
|
+
cfg := config.Load()
|
|
105
|
+
cfg.BaseURL, cfg.AccessKey = server.URL, "image-model-test-key"
|
|
106
|
+
root := newRootCommand(&stdout, &stderr, newRootRunner(cfg))
|
|
107
|
+
root.SetArgs(args)
|
|
108
|
+
if err := root.Execute(); err != nil {
|
|
109
|
+
t.Fatalf("%v: %v", args, err)
|
|
110
|
+
}
|
|
111
|
+
if !strings.Contains(stdout.String(), `"scene": "web_image_agent"`) && !strings.Contains(stdout.String(), `"scene":"web_image_agent"`) {
|
|
112
|
+
t.Fatalf("missing image scene: %s", stdout.String())
|
|
113
|
+
}
|
|
114
|
+
if strings.Contains(stdout.String(), `"is_default"`) || strings.Contains(stdout.String(), `"config_key"`) || strings.Contains(stdout.String(), "image-model") {
|
|
115
|
+
t.Fatalf("internal fields exposed: %s", stdout.String())
|
|
116
|
+
}
|
|
117
|
+
if args[1] == "describe" || args[1] == "图片测试模型" {
|
|
118
|
+
var output struct {
|
|
119
|
+
Model struct {
|
|
120
|
+
Effort struct{ Options []string }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if err := json.Unmarshal(stdout.Bytes(), &output); err != nil || len(output.Model.Effort.Options) != 2 {
|
|
124
|
+
t.Fatalf("missing effort choices: %s err=%v", stdout.String(), err)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if requests != 1 {
|
|
129
|
+
t.Fatalf("commands did not share image cache: %d requests", requests)
|
|
130
|
+
}
|
|
131
|
+
var stdout, stderr bytes.Buffer
|
|
132
|
+
cfg := config.Load()
|
|
133
|
+
cfg.BaseURL, cfg.AccessKey = server.URL, "image-model-test-key"
|
|
134
|
+
root := newRootCommand(&stdout, &stderr, newRootRunner(cfg))
|
|
135
|
+
root.SetArgs([]string{"model", "list", "--type", "audio"})
|
|
136
|
+
if err := root.Execute(); err == nil || requests != 1 {
|
|
137
|
+
t.Fatalf("invalid type must fail before request: err=%v requests=%d", err, requests)
|
|
138
|
+
}
|
|
139
|
+
}
|
package/cmd/short_drama_test.go
CHANGED
|
@@ -11,6 +11,7 @@ import (
|
|
|
11
11
|
"testing"
|
|
12
12
|
"time"
|
|
13
13
|
|
|
14
|
+
"github.com/Pippit-dev/pippit-cli/internal/auth"
|
|
14
15
|
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
15
16
|
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
16
17
|
"github.com/Pippit-dev/pippit-cli/internal/version"
|
|
@@ -963,6 +964,7 @@ func newTestRootCommandWithAccessKey(t *testing.T, stdout, stderr io.Writer, bas
|
|
|
963
964
|
cfg.AccessKey = accessKey
|
|
964
965
|
client := common.NewHTTPClient(cfg.BaseURL, cfg.HTTPTimeout, common.NewAccessKeyAuthorizer(cfg.AccessKey))
|
|
965
966
|
runner := common.NewRunner(cfg, client)
|
|
967
|
+
runner.Auth = auth.NewManager(cfg)
|
|
966
968
|
return newRootCommand(stdout, stderr, runner)
|
|
967
969
|
}
|
|
968
970
|
|
package/cmd/source_test.go
CHANGED
|
@@ -16,6 +16,9 @@ import (
|
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
func TestSkillSubmitSourcePreservesCreativeRequest(t *testing.T) {
|
|
19
|
+
t.Setenv("HOME", t.TempDir())
|
|
20
|
+
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
|
21
|
+
t.Setenv("LocalAppData", t.TempDir())
|
|
19
22
|
for _, name := range []string{"submit-run", "generate-image", "generate-video", "video-super-resolution", "erase-video-subtitle", "short-drama"} {
|
|
20
23
|
t.Run(name, func(t *testing.T) {
|
|
21
24
|
video := filepath.Join(t.TempDir(), "input.mp4")
|
|
@@ -24,7 +27,7 @@ func TestSkillSubmitSourcePreservesCreativeRequest(t *testing.T) {
|
|
|
24
27
|
}
|
|
25
28
|
args := map[string][]string{
|
|
26
29
|
"submit-run": {"submit-run", "--message", " 保留原文\nworkbuddy 只是内容 ", "--thread-id", "skill_existing", "--asset-ids", "asset_1"},
|
|
27
|
-
"generate-image": {"generate-image", "--prompt", "cat", "--model", "
|
|
30
|
+
"generate-image": {"generate-image", "--prompt", "cat", "--model", "Seedream 5.0 Pro", "--ratio", "6", "--resolution", "4K"},
|
|
28
31
|
"generate-video": {"generate-video", "--prompt", "cat", "--model", "Seedance_2.0_mini", "--duration", "4", "--ratio", "16:9"},
|
|
29
32
|
"video-super-resolution": {"video-super-resolution", "--video", video, "--output-resolution", "1080p"},
|
|
30
33
|
"erase-video-subtitle": {"erase-video-subtitle", "--video", video},
|
|
@@ -37,6 +40,8 @@ func TestSkillSubmitSourcePreservesCreativeRequest(t *testing.T) {
|
|
|
37
40
|
uploadFailure := false
|
|
38
41
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
39
42
|
switch r.URL.Path {
|
|
43
|
+
case config.GetAvailableModelListPath:
|
|
44
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"scene":"web_image_agent","config_key":"image-key","config":{"models":[{"key":"seedream_5.0_pro","name":"Seedream 5.0 Pro","kind":"image"}]}}}`))
|
|
40
45
|
case config.SubmitRunPath:
|
|
41
46
|
submitCount++
|
|
42
47
|
if r.Method != http.MethodPost || r.Header.Get("Authorization") != "Bearer test-token" {
|
package/dist/checksums.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
dd742dedf06e2645d02bc13a4cfa1af45778ae0200a8a6e67df177ba7e069775 pippit-tool-cli-1.0.33-darwin-amd64.tar.gz
|
|
2
|
+
8d9fc0112650839c430e58cb7d6917e5bb1b95fde49659aecbbf14d1421c28f5 pippit-tool-cli-1.0.33-darwin-arm64.tar.gz
|
|
3
|
+
f2f2f741c920b115b2a1a9c4c4f074c1a07fd6cbc3a442cf4dc4d793c1b9ef5a pippit-tool-cli-1.0.33-linux-amd64.tar.gz
|
|
4
|
+
2ed6e791c7dbea0e2ea8a1d05a0db298442218ecfc17ec47d70d49639f1d91b6 pippit-tool-cli-1.0.33-linux-arm64.tar.gz
|
|
5
|
+
f713c05325043f177f4a9e01a433b6a7fc8f57fea123f712ee459707955d199e pippit-tool-cli-1.0.33-windows-amd64.zip
|
|
6
|
+
7e637f67fbbb818db0e66c49f7814d7e79cd71857d8c3bd6a5397af9d18533ce pippit-tool-cli-1.0.33-windows-arm64.zip
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
package common
|
|
2
|
+
|
|
3
|
+
// RatioValue mirrors capcut_business_common.Ratio. Custom (1) has no CLI
|
|
4
|
+
// width/height parameters, so it and unknown enums have no display value.
|
|
5
|
+
func RatioValue(value int64) string {
|
|
6
|
+
return ratioValues[value]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
var ratioValues = map[int64]string{
|
|
10
|
+
0: "adaptive", 2: "16:9", 3: "9:16", 4: "4:3", 5: "3:4", 6: "1:1",
|
|
11
|
+
7: "2:1", 8: "2.35:1", 9: "1.85:1", 10: "1.125:2.436", 11: "3:2", 12: "2:3", 13: "21:9",
|
|
12
|
+
}
|
|
@@ -8,6 +8,7 @@ import (
|
|
|
8
8
|
"strings"
|
|
9
9
|
|
|
10
10
|
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
11
|
+
"github.com/Pippit-dev/pippit-cli/internal/models"
|
|
11
12
|
)
|
|
12
13
|
|
|
13
14
|
const (
|
|
@@ -19,7 +20,7 @@ var (
|
|
|
19
20
|
allowedImageExtensions = common.StringSet(allowedImageExtensionList)
|
|
20
21
|
)
|
|
21
22
|
|
|
22
|
-
const ratioUsage = "enum
|
|
23
|
+
const ratioUsage = "integer enum from 'model describe \"MODEL_NAME\" --type image' (e.g. 0=adaptive, 2=16:9, 3=9:16, 6=1:1)"
|
|
23
24
|
|
|
24
25
|
// Options is the stable command-facing request shape for generate-image.
|
|
25
26
|
type Options struct {
|
|
@@ -29,6 +30,7 @@ type Options struct {
|
|
|
29
30
|
Model string
|
|
30
31
|
Ratio string
|
|
31
32
|
Resolution string
|
|
33
|
+
Effort string
|
|
32
34
|
GenerateImageCount *int
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -36,6 +38,7 @@ type generalAgentSettings struct {
|
|
|
36
38
|
ImageModel string `json:"image_model"`
|
|
37
39
|
Ratio *int `json:"ratio,omitempty"`
|
|
38
40
|
Resolution string `json:"resolution,omitempty"`
|
|
41
|
+
ImageEffort string `json:"image_effort,omitempty"`
|
|
39
42
|
GenerateImageCount *int `json:"generate_image_count,omitempty"`
|
|
40
43
|
}
|
|
41
44
|
|
|
@@ -53,23 +56,31 @@ func Run(ctx context.Context, opts *Options, runner *common.Runner) (*Result, er
|
|
|
53
56
|
if err := ValidateOptions(opts); err != nil {
|
|
54
57
|
return nil, err
|
|
55
58
|
}
|
|
59
|
+
catalog, err := models.NewService(runner).Get(ctx, "image", false)
|
|
60
|
+
if err != nil {
|
|
61
|
+
return nil, err
|
|
62
|
+
}
|
|
63
|
+
modelKey, err := catalog.Catalog.ImageModelKey(opts.Model)
|
|
64
|
+
if err != nil {
|
|
65
|
+
return nil, err
|
|
66
|
+
}
|
|
56
67
|
|
|
57
68
|
imageAssetIDs, err := uploadImageList(ctx, opts.ImagePaths, runner)
|
|
58
69
|
if err != nil {
|
|
59
70
|
return nil, fmt.Errorf("上传图片失败: %w", err)
|
|
60
71
|
}
|
|
61
72
|
|
|
62
|
-
body := buildSubmitRunBody(opts, imageAssetIDs)
|
|
73
|
+
body := buildSubmitRunBody(opts, modelKey, imageAssetIDs)
|
|
63
74
|
|
|
64
75
|
var resp common.SubmitRunResponse
|
|
65
76
|
if err := runner.Client.SendRequest(ctx, common.SubmitRunPath(runner), body, &resp); err != nil {
|
|
66
|
-
return nil, fmt.Errorf("提交 generate-image 请求失败: %
|
|
77
|
+
return nil, fmt.Errorf("提交 generate-image 请求失败: %s", catalog.Catalog.ImageDisplayMessage(err.Error()))
|
|
67
78
|
}
|
|
68
79
|
if resp.Ret != "0" {
|
|
69
80
|
if resp.Errmsg == "" {
|
|
70
81
|
resp.Errmsg = "未知错误"
|
|
71
82
|
}
|
|
72
|
-
return nil, common.NewLogIDError(fmt.Sprintf("generate-image 请求返回失败: ret=%s errmsg=%s", resp.Ret, resp.Errmsg), resp.LogID)
|
|
83
|
+
return nil, common.NewLogIDError(fmt.Sprintf("generate-image 请求返回失败: ret=%s errmsg=%s", resp.Ret, catalog.Catalog.ImageDisplayMessage(resp.Errmsg)), resp.LogID)
|
|
73
84
|
}
|
|
74
85
|
if resp.Data.Run.ThreadID == "" {
|
|
75
86
|
return nil, fmt.Errorf("generate-image 响应缺少 data.run.thread_id")
|
|
@@ -137,15 +148,16 @@ func uploadImageList(ctx context.Context, paths []string, runner *common.Runner)
|
|
|
137
148
|
return assetIDs, nil
|
|
138
149
|
}
|
|
139
150
|
|
|
140
|
-
func buildSubmitRunBody(opts *Options, imageAssetIDs []string) map[string]any {
|
|
151
|
+
func buildSubmitRunBody(opts *Options, modelKey string, imageAssetIDs []string) map[string]any {
|
|
141
152
|
ratio, _ := parseRatio(opts.Ratio)
|
|
142
153
|
body := map[string]any{
|
|
143
154
|
"agent_name": agentNameNest,
|
|
144
155
|
"message": strings.TrimSpace(opts.Prompt),
|
|
145
156
|
"general_agent_settings": generalAgentSettings{
|
|
146
|
-
ImageModel:
|
|
157
|
+
ImageModel: modelKey,
|
|
147
158
|
Ratio: ratio,
|
|
148
159
|
Resolution: strings.ToUpper(strings.TrimSpace(opts.Resolution)),
|
|
160
|
+
ImageEffort: strings.ToLower(strings.TrimSpace(opts.Effort)),
|
|
149
161
|
GenerateImageCount: opts.GenerateImageCount,
|
|
150
162
|
},
|
|
151
163
|
}
|
|
@@ -162,7 +174,7 @@ func parseRatio(raw string) (*int, error) {
|
|
|
162
174
|
}
|
|
163
175
|
value, err := strconv.Atoi(ratio)
|
|
164
176
|
if err != nil {
|
|
165
|
-
return nil, fmt.Errorf("ratio %q
|
|
177
|
+
return nil, fmt.Errorf("ratio %q 必须是整数枚举值(如 3 表示 9:16);请用 model describe \"模型名称\" --type image 查询", ratio)
|
|
166
178
|
}
|
|
167
179
|
return &value, nil
|
|
168
180
|
}
|