@pippit-dev/cli 1.0.31 → 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 +20 -9
- package/checksums.txt +6 -6
- package/cmd/generate_image/generate_image.go +4 -2
- package/cmd/generate_image_test.go +150 -1
- package/cmd/marketing.go +144 -0
- package/cmd/marketing_test.go +179 -0
- package/cmd/model.go +7 -10
- package/cmd/model_test.go +59 -1
- package/cmd/root.go +1 -0
- package/cmd/short_drama_test.go +2 -0
- package/cmd/source_test.go +6 -1
- package/dist/checksums.txt +6 -6
- package/internal/common/client.go +11 -1
- 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-marketing-skill/SKILL.md +8 -6
- package/skills/xyq-marketing-skill/examples/product-video.md +1 -1
- package/skills/xyq-marketing-skill/references/api.md +2 -2
- package/skills/xyq-marketing-skill/scripts/marketing.js +76 -38
- 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/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/root.go
CHANGED
|
@@ -73,6 +73,7 @@ func newRootCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Comm
|
|
|
73
73
|
root.AddCommand(newDownloadResultCommand(stdout, stderr, runner))
|
|
74
74
|
root.AddCommand(newGetCreditBalanceCommand(stdout, stderr, runner))
|
|
75
75
|
root.AddCommand(newModelCommand(stdout, stderr, runner))
|
|
76
|
+
root.AddCommand(newMarketingCommand(stdout, stderr, runner))
|
|
76
77
|
root.AddCommand(newGetThreadCommand(stdout, stderr, runner))
|
|
77
78
|
root.AddCommand(newSubmitRunCommand(stdout, stderr, runner))
|
|
78
79
|
root.AddCommand(newUploadFileCommand(stdout, stderr, runner))
|
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
|
|
@@ -46,7 +46,17 @@ func NewHTTPClient(baseURL string, timeout time.Duration, authorizer RequestAuth
|
|
|
46
46
|
return newHTTPClient(baseURL, timeout, authorizer)
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
// NewNonRedirectingHTTPClient keeps one-shot submissions from being replayed.
|
|
50
|
+
// Authentication and request handling still use the shared CLI client.
|
|
51
|
+
func NewNonRedirectingHTTPClient(baseURL string, timeout time.Duration, authorizer RequestAuthorizer) Client {
|
|
52
|
+
client := newHTTPClient(baseURL, timeout, authorizer)
|
|
53
|
+
client.httpClient.CheckRedirect = func(_ *http.Request, _ []*http.Request) error {
|
|
54
|
+
return fmt.Errorf("拒绝营销 API 重定向;请求未重试")
|
|
55
|
+
}
|
|
56
|
+
return client
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func newHTTPClient(baseURL string, timeout time.Duration, authorizer RequestAuthorizer) *httpClient {
|
|
50
60
|
client := &httpClient{
|
|
51
61
|
baseURL: strings.TrimRight(baseURL, "/"),
|
|
52
62
|
headers: make(http.Header),
|
|
@@ -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
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package generate_image
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"encoding/json"
|
|
4
5
|
"strings"
|
|
5
6
|
"testing"
|
|
6
7
|
)
|
|
@@ -19,10 +20,10 @@ func TestValidateOptionsRequiresModel(t *testing.T) {
|
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
func
|
|
23
|
+
func TestValidateOptionsAllowsDynamicModelName(t *testing.T) {
|
|
23
24
|
opts := &Options{
|
|
24
25
|
Prompt: "x",
|
|
25
|
-
Model: "
|
|
26
|
+
Model: "图片测试模型",
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
if err := ValidateOptions(opts); err != nil {
|
|
@@ -33,7 +34,7 @@ func TestValidateOptionsAllowsServerDecidedModel(t *testing.T) {
|
|
|
33
34
|
func TestValidateOptionsAllowsServerDecidedRatio(t *testing.T) {
|
|
34
35
|
opts := &Options{
|
|
35
36
|
Prompt: "x",
|
|
36
|
-
Model: "
|
|
37
|
+
Model: "图片测试模型",
|
|
37
38
|
Ratio: "99",
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -45,7 +46,7 @@ func TestValidateOptionsAllowsServerDecidedRatio(t *testing.T) {
|
|
|
45
46
|
func TestValidateOptionsAllowsServerDecidedResolution(t *testing.T) {
|
|
46
47
|
opts := &Options{
|
|
47
48
|
Prompt: "x",
|
|
48
|
-
Model: "
|
|
49
|
+
Model: "图片测试模型",
|
|
49
50
|
Resolution: "8K",
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -58,7 +59,7 @@ func TestValidateOptionsRejectsNegativeGenerateImageCount(t *testing.T) {
|
|
|
58
59
|
count := -1
|
|
59
60
|
opts := &Options{
|
|
60
61
|
Prompt: "x",
|
|
61
|
-
Model: "
|
|
62
|
+
Model: "图片测试模型",
|
|
62
63
|
GenerateImageCount: &count,
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -83,6 +84,13 @@ func TestParseRatioSupportsVisibleEnumValues(t *testing.T) {
|
|
|
83
84
|
{ratio: "4", want: 4},
|
|
84
85
|
{ratio: "5", want: 5},
|
|
85
86
|
{ratio: "6", want: 6},
|
|
87
|
+
{ratio: "7", want: 7},
|
|
88
|
+
{ratio: "8", want: 8},
|
|
89
|
+
{ratio: "9", want: 9},
|
|
90
|
+
{ratio: "10", want: 10},
|
|
91
|
+
{ratio: "11", want: 11},
|
|
92
|
+
{ratio: "12", want: 12},
|
|
93
|
+
{ratio: "99", want: 99},
|
|
86
94
|
}
|
|
87
95
|
|
|
88
96
|
for _, tt := range cases {
|
|
@@ -98,20 +106,55 @@ func TestParseRatioSupportsVisibleEnumValues(t *testing.T) {
|
|
|
98
106
|
}
|
|
99
107
|
}
|
|
100
108
|
|
|
101
|
-
func
|
|
102
|
-
_,
|
|
103
|
-
|
|
104
|
-
|
|
109
|
+
func TestImageOptionalParametersAndEffortPassThrough(t *testing.T) {
|
|
110
|
+
for _, effort := range []string{"", " HIGH ", "future-effort"} {
|
|
111
|
+
opts := &Options{Prompt: "image", Model: "未来图片模型", Effort: effort}
|
|
112
|
+
if err := ValidateOptions(opts); err != nil {
|
|
113
|
+
t.Fatal(err)
|
|
114
|
+
}
|
|
115
|
+
raw, err := json.Marshal(buildSubmitRunBody(opts, "future-image-model", nil))
|
|
116
|
+
if err != nil {
|
|
117
|
+
t.Fatal(err)
|
|
118
|
+
}
|
|
119
|
+
var body struct {
|
|
120
|
+
Settings map[string]json.RawMessage `json:"general_agent_settings"`
|
|
121
|
+
}
|
|
122
|
+
if err := json.Unmarshal(raw, &body); err != nil {
|
|
123
|
+
t.Fatal(err)
|
|
124
|
+
}
|
|
125
|
+
for _, key := range []string{"ratio", "resolution", "generate_image_count"} {
|
|
126
|
+
if _, exists := body.Settings[key]; exists {
|
|
127
|
+
t.Fatalf("unspecified %s was filled: %s", key, raw)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if effort == "" {
|
|
131
|
+
if _, exists := body.Settings["image_effort"]; exists {
|
|
132
|
+
t.Fatalf("unspecified effort was filled: %s", raw)
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
var got string
|
|
136
|
+
if err := json.Unmarshal(body.Settings["image_effort"], &got); err != nil || got != strings.ToLower(strings.TrimSpace(effort)) {
|
|
137
|
+
t.Fatalf("effort not forwarded: %s, %v", raw, err)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
105
140
|
}
|
|
106
|
-
|
|
107
|
-
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func TestParseRatioRejectsNonInteger(t *testing.T) {
|
|
144
|
+
for _, ratio := range []string{"9:16", "adaptive", "17:11", "3.0"} {
|
|
145
|
+
t.Run(ratio, func(t *testing.T) {
|
|
146
|
+
err := ValidateOptions(&Options{Prompt: "image", Model: "未来图片模型", Ratio: ratio})
|
|
147
|
+
if err == nil || !strings.Contains(err.Error(), "必须是整数枚举值") {
|
|
148
|
+
t.Fatalf("ratio %q: error = %v, want integer enum validation", ratio, err)
|
|
149
|
+
}
|
|
150
|
+
})
|
|
108
151
|
}
|
|
109
152
|
}
|
|
110
153
|
|
|
111
154
|
func TestValidateOptionsRejectsUnsupportedImageExtension(t *testing.T) {
|
|
112
155
|
opts := &Options{
|
|
113
156
|
Prompt: "x",
|
|
114
|
-
Model: "
|
|
157
|
+
Model: "图片测试模型",
|
|
115
158
|
ImagePaths: []string{"ref.tiff"},
|
|
116
159
|
}
|
|
117
160
|
|
|
@@ -128,13 +171,13 @@ func TestBuildSubmitRunBodyWithGeneralAgentSettings(t *testing.T) {
|
|
|
128
171
|
count := 2
|
|
129
172
|
opts := &Options{
|
|
130
173
|
Prompt: " 生成小猫海报 ",
|
|
131
|
-
Model: "
|
|
174
|
+
Model: " Seedream 5.0 Pro ",
|
|
132
175
|
Ratio: "6",
|
|
133
176
|
Resolution: " 4k ",
|
|
134
177
|
GenerateImageCount: &count,
|
|
135
178
|
}
|
|
136
179
|
|
|
137
|
-
body := buildSubmitRunBody(opts, []string{"asset_1"})
|
|
180
|
+
body := buildSubmitRunBody(opts, "seedream_5.0_pro", []string{"asset_1"})
|
|
138
181
|
if body["agent_name"] != agentNameNest {
|
|
139
182
|
t.Fatalf("agent_name = %v, want nest agent", body["agent_name"])
|
|
140
183
|
}
|
|
@@ -6,17 +6,15 @@ import (
|
|
|
6
6
|
"slices"
|
|
7
7
|
"strconv"
|
|
8
8
|
"strings"
|
|
9
|
-
)
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
var ratioValues = map[int64]string{
|
|
14
|
-
0: "adaptive", 1: "", 2: "16:9", 3: "9:16", 4: "4:3", 5: "3:4", 6: "1:1",
|
|
15
|
-
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",
|
|
16
|
-
}
|
|
10
|
+
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
11
|
+
)
|
|
17
12
|
|
|
18
13
|
type dimensionConfig struct {
|
|
19
14
|
Key string `json:"key"`
|
|
15
|
+
Label string `json:"label"`
|
|
16
|
+
Description string `json:"description"`
|
|
17
|
+
Required *bool `json:"required_field"`
|
|
20
18
|
DefaultValue *string `json:"default_value"`
|
|
21
19
|
OptionList []struct {
|
|
22
20
|
Value string `json:"value"`
|
|
@@ -36,6 +34,8 @@ func describeModel(raw json.RawMessage) (json.RawMessage, error) {
|
|
|
36
34
|
var fields map[string]json.RawMessage
|
|
37
35
|
var source struct {
|
|
38
36
|
Key string `json:"key"`
|
|
37
|
+
Name string `json:"name"`
|
|
38
|
+
Kind string `json:"kind"`
|
|
39
39
|
Ratios []int64 `json:"supported_ratio_list"`
|
|
40
40
|
Default *int64 `json:"default_ratio"`
|
|
41
41
|
Parameter struct {
|
|
@@ -57,18 +57,27 @@ func describeModel(raw json.RawMessage) (json.RawMessage, error) {
|
|
|
57
57
|
}
|
|
58
58
|
delete(out, "config_key")
|
|
59
59
|
delete(out, "is_default") // A server default does not authorize model selection.
|
|
60
|
+
if source.Kind == "image" {
|
|
61
|
+
// Billing maps and other Web-only metadata can contain wire model names.
|
|
62
|
+
// Keep them in the raw catalog, not in host-facing command output.
|
|
63
|
+
out = map[string]any{"name": strings.TrimSpace(source.Name), "kind": source.Kind}
|
|
64
|
+
for _, key := range []string{"description", "parameter_config", "creation_mode_config"} {
|
|
65
|
+
if value, exists := fields[key]; exists {
|
|
66
|
+
out[key] = value
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
60
70
|
warnings := []string{}
|
|
61
71
|
warn := func(message string) { warnings = append(warnings, message) }
|
|
62
|
-
ratios := make([]
|
|
72
|
+
ratios := make([]any, 0, len(source.Ratios))
|
|
63
73
|
for _, value := range source.Ratios {
|
|
64
|
-
|
|
65
|
-
if value := ratioValues[value]; value != "" && !slices.Contains(ratios, value) {
|
|
74
|
+
if value, ok := cliRatioValue(value, source.Kind); ok && !slices.Contains(ratios, value) {
|
|
66
75
|
ratios = append(ratios, value)
|
|
67
76
|
}
|
|
68
77
|
}
|
|
69
78
|
ratio := map[string]any{"options": ratios}
|
|
70
79
|
if source.Default != nil {
|
|
71
|
-
if value :=
|
|
80
|
+
if value, ok := cliRatioValue(*source.Default, source.Kind); ok {
|
|
72
81
|
if slices.Contains(ratios, value) {
|
|
73
82
|
ratio["default"] = value
|
|
74
83
|
} else {
|
|
@@ -84,17 +93,37 @@ func describeModel(raw json.RawMessage) (json.RawMessage, error) {
|
|
|
84
93
|
|
|
85
94
|
seen := make(map[string]bool)
|
|
86
95
|
for _, dimension := range source.Parameter.Dimensions {
|
|
87
|
-
if dimension == nil
|
|
96
|
+
if dimension == nil {
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
if source.Kind == "image" {
|
|
100
|
+
if dimension.Key != "resolution" && dimension.Key != "ratio" && dimension.Key != "effort" {
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
} else if dimension.Key != "resolution" && dimension.Key != "duration" {
|
|
88
104
|
continue
|
|
89
105
|
}
|
|
90
106
|
if seen[dimension.Key] {
|
|
91
107
|
return nil, fmt.Errorf("模型配置包含重复 %s 维度,请 --refresh 重试", dimension.Key)
|
|
92
108
|
}
|
|
93
109
|
seen[dimension.Key] = true
|
|
94
|
-
out[dimension.Key] = describeDimension(dimension, warn)
|
|
110
|
+
out[dimension.Key] = describeDimension(dimension, source.Kind, warn)
|
|
111
|
+
}
|
|
112
|
+
if source.Kind == "image" {
|
|
113
|
+
if imageRatio, ok := out["ratio"].(map[string]any); ok {
|
|
114
|
+
if options, ok := imageRatio["options"].([]any); ok {
|
|
115
|
+
labels := make(map[string]string, len(options))
|
|
116
|
+
for _, option := range options {
|
|
117
|
+
value := option.(int64)
|
|
118
|
+
labels[strconv.FormatInt(value, 10)] = common.RatioValue(value)
|
|
119
|
+
}
|
|
120
|
+
imageRatio["option_labels"] = labels
|
|
121
|
+
}
|
|
122
|
+
}
|
|
95
123
|
}
|
|
96
|
-
//
|
|
97
|
-
|
|
124
|
+
// Image dimensions retain their complete source metadata and combination rules.
|
|
125
|
+
// Video descriptions keep the existing compact representation.
|
|
126
|
+
if parameterRaw, ok := fields["parameter_config"]; source.Kind != "image" && ok && string(parameterRaw) != "null" {
|
|
98
127
|
var parameter map[string]json.RawMessage
|
|
99
128
|
if err := json.Unmarshal(parameterRaw, ¶meter); err != nil {
|
|
100
129
|
return nil, err
|
|
@@ -146,7 +175,7 @@ func describeModel(raw json.RawMessage) (json.RawMessage, error) {
|
|
|
146
175
|
if len(limits) > 0 {
|
|
147
176
|
out["material_limits"] = limits
|
|
148
177
|
}
|
|
149
|
-
if source.Creation != nil {
|
|
178
|
+
if source.Creation != nil && source.Kind != "image" {
|
|
150
179
|
modes := make([]map[string]any, 0, len(source.Creation.Modes))
|
|
151
180
|
for _, rawMode := range source.Creation.Modes {
|
|
152
181
|
if string(rawMode) == "null" {
|
|
@@ -181,7 +210,7 @@ func describeModel(raw json.RawMessage) (json.RawMessage, error) {
|
|
|
181
210
|
case "", "inherit":
|
|
182
211
|
entry["ratio"] = ratio
|
|
183
212
|
if mode.Key == "text_to_video" && (source.Key == "MiniMax-H3" || source.Key == "MiniMax-H3-Max") {
|
|
184
|
-
fixed := make([]
|
|
213
|
+
fixed := make([]any, 0, len(ratios))
|
|
185
214
|
for _, value := range ratios {
|
|
186
215
|
if value != "adaptive" {
|
|
187
216
|
fixed = append(fixed, value)
|
|
@@ -210,7 +239,7 @@ func describeModel(raw json.RawMessage) (json.RawMessage, error) {
|
|
|
210
239
|
}
|
|
211
240
|
out["creation_modes"] = modes
|
|
212
241
|
}
|
|
213
|
-
if (source.Key == "MiniMax-H3" || source.Key == "MiniMax-H3-Max") && slices.Contains(ratios, "adaptive") {
|
|
242
|
+
if source.Kind != "image" && (source.Key == "MiniMax-H3" || source.Key == "MiniMax-H3-Max") && slices.Contains(ratios, "adaptive") {
|
|
214
243
|
out["notes"] = []string{"MiniMax 使用 adaptive 需要参考图片或视频;纯文生视频请选择固定比例。创作模式的比例策略优先于模型级选项。"}
|
|
215
244
|
}
|
|
216
245
|
if len(warnings) > 0 {
|
|
@@ -219,8 +248,29 @@ func describeModel(raw json.RawMessage) (json.RawMessage, error) {
|
|
|
219
248
|
return json.Marshal(out)
|
|
220
249
|
}
|
|
221
250
|
|
|
222
|
-
|
|
251
|
+
// Image generation accepts wire enums; video generation accepts ratio strings.
|
|
252
|
+
func cliRatioValue(value int64, kind string) (any, bool) {
|
|
253
|
+
label := common.RatioValue(value)
|
|
254
|
+
if label == "" {
|
|
255
|
+
return nil, false
|
|
256
|
+
}
|
|
257
|
+
if kind == "image" {
|
|
258
|
+
return value, true
|
|
259
|
+
}
|
|
260
|
+
return label, true
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
func describeDimension(d *dimensionConfig, kind string, warn func(string)) map[string]any {
|
|
223
264
|
out := map[string]any{}
|
|
265
|
+
if d.Label != "" {
|
|
266
|
+
out["label"] = d.Label
|
|
267
|
+
}
|
|
268
|
+
if d.Description != "" {
|
|
269
|
+
out["description"] = d.Description
|
|
270
|
+
}
|
|
271
|
+
if d.Required != nil {
|
|
272
|
+
out["required_field"] = *d.Required
|
|
273
|
+
}
|
|
224
274
|
if len(d.ActiveWhenAny) > 0 {
|
|
225
275
|
out["active_when_any"] = d.ActiveWhenAny
|
|
226
276
|
}
|
|
@@ -230,6 +280,16 @@ func describeDimension(d *dimensionConfig, warn func(string)) map[string]any {
|
|
|
230
280
|
number, err := strconv.ParseInt(value, 10, 32)
|
|
231
281
|
return number, err == nil && number > 0
|
|
232
282
|
}
|
|
283
|
+
if d.Key == "ratio" {
|
|
284
|
+
number, err := strconv.ParseInt(value, 10, 64)
|
|
285
|
+
if err != nil {
|
|
286
|
+
return nil, false
|
|
287
|
+
}
|
|
288
|
+
return cliRatioValue(number, kind)
|
|
289
|
+
}
|
|
290
|
+
if kind == "image" && d.Key == "resolution" {
|
|
291
|
+
return strings.ToUpper(value), value != ""
|
|
292
|
+
}
|
|
233
293
|
return strings.ToLower(value), value != ""
|
|
234
294
|
}
|
|
235
295
|
if d.Key == "duration" {
|