@pippit-dev/cli 1.0.26 → 1.0.28
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 +23 -1
- package/checksums.txt +6 -6
- package/cmd/generate_image/generate_image.go +1 -0
- package/cmd/generate_video/generate_video.go +4 -3
- package/cmd/model.go +74 -0
- package/cmd/model_test.go +81 -0
- package/cmd/root.go +1 -0
- package/cmd/short_drama/short_drama.go +1 -0
- package/cmd/source_test.go +179 -0
- package/cmd/submit_run.go +3 -0
- package/cmd/update/update.go +1 -1
- package/cmd/video_tool/video_tool.go +2 -0
- package/dist/checksums.txt +6 -6
- package/internal/common/submit_run.go +38 -1
- package/internal/config/config.go +13 -10
- package/internal/config/config_test.go +3 -0
- package/internal/generate_image/generate_image.go +2 -1
- package/internal/generate_video/generate_video.go +12 -3
- package/internal/generate_video/generate_video_test.go +126 -0
- package/internal/models/describe.go +281 -0
- package/internal/models/describe_test.go +199 -0
- package/internal/models/models.go +220 -0
- package/internal/models/models_test.go +200 -0
- package/internal/short_drama/submit_run.go +2 -0
- package/internal/video_tool/video_tool.go +4 -2
- package/package.json +1 -1
- package/scripts/install.js +3 -2
- package/skills/short-drama/SKILL.md +4 -0
- package/skills/xyq-nest-skill/SKILL.md +10 -0
- package/skills/xyq-nest-skill/commands/erase-video-subtitle.md +2 -0
- package/skills/xyq-nest-skill/commands/generate-image.md +2 -0
- package/skills/xyq-nest-skill/commands/generate-video.md +31 -7
- package/skills/xyq-nest-skill/commands/model.md +59 -0
- package/skills/xyq-nest-skill/commands/video-super-resolution.md +2 -0
- package/skills/xyq-nest-skill/scripts/ensure-cli.js +3 -3
- package/skills/xyq-nest-skill/scripts/install.md +1 -1
|
@@ -18,6 +18,7 @@ var (
|
|
|
18
18
|
|
|
19
19
|
// Options is the stable command-facing request shape for generate-video.
|
|
20
20
|
type Options struct {
|
|
21
|
+
Source string
|
|
21
22
|
Prompt string
|
|
22
23
|
ImagePaths []string
|
|
23
24
|
VideoPaths []string
|
|
@@ -54,7 +55,15 @@ func Run(ctx context.Context, opts *Options, runner *common.Runner) (*Result, er
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
body := buildSubmitRunBody(opts, imageAssetIDs, videoAssetIDs, audioAssetIDs)
|
|
57
|
-
|
|
58
|
+
// Business attribution belongs to this command, regardless of the selected model.
|
|
59
|
+
return common.SubmitRunWithBabiParam(ctx, "generate-video", body, runner, map[string]string{
|
|
60
|
+
"scene_lv1": "ai_agent",
|
|
61
|
+
"scene_lv2": "front_tool",
|
|
62
|
+
"tool_id": "instant_video",
|
|
63
|
+
"tab_name": "other",
|
|
64
|
+
"edit_type": "instant_video",
|
|
65
|
+
"enter_from": "skill",
|
|
66
|
+
})
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
func ValidateOptions(opts *Options) error {
|
|
@@ -116,11 +125,11 @@ func buildSubmitRunBody(opts *Options, imageAssetIDs []string, videoAssetIDs []s
|
|
|
116
125
|
GenerateType: opts.GenerateType,
|
|
117
126
|
}
|
|
118
127
|
|
|
119
|
-
return map[string]any{
|
|
128
|
+
return common.WithSubmitRunSource(map[string]any{
|
|
120
129
|
"agent_name": common.AgentNameVideoPart,
|
|
121
130
|
"message": prompt,
|
|
122
131
|
"video_part_tool_param": param,
|
|
123
|
-
}
|
|
132
|
+
}, opts.Source)
|
|
124
133
|
}
|
|
125
134
|
|
|
126
135
|
func assetRefs(assetIDs []string) []*common.MediaAsset {
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
package generate_video
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"context"
|
|
4
5
|
"encoding/json"
|
|
6
|
+
"net/url"
|
|
5
7
|
"testing"
|
|
8
|
+
|
|
9
|
+
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
10
|
+
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
6
11
|
)
|
|
7
12
|
|
|
8
13
|
func TestBuildSubmitRunBodyPreservesEmptyPrompt(t *testing.T) {
|
|
@@ -15,3 +20,124 @@ func TestBuildSubmitRunBodyPreservesEmptyPrompt(t *testing.T) {
|
|
|
15
20
|
t.Fatalf("video_part_tool_param = %s, want explicit empty prompt", got)
|
|
16
21
|
}
|
|
17
22
|
}
|
|
23
|
+
|
|
24
|
+
type videoSubmitRecordingClient struct {
|
|
25
|
+
common.Client
|
|
26
|
+
path string
|
|
27
|
+
body any
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func (c *videoSubmitRecordingClient) SendRequest(_ context.Context, path string, body, out any) error {
|
|
31
|
+
c.path, c.body = path, body
|
|
32
|
+
return json.Unmarshal([]byte(`{"ret":"0","data":{"run":{"thread_id":"thread_123","run_id":"run_123"}}}`), out)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func TestRunCarriesVideoROIQuery(t *testing.T) {
|
|
36
|
+
for _, model := range []string{
|
|
37
|
+
"MiniMax-H3", "MiniMax-H3-Max", "wan3.0", "happyhorse-1.1",
|
|
38
|
+
"Seedance_2.5", "seedance2.0_vision", "seedance2.0_fast_vision",
|
|
39
|
+
"Seedance_2.0_mini", "Seedance_2.0_mini_lite", "", "future-model",
|
|
40
|
+
} {
|
|
41
|
+
t.Run(model, func(t *testing.T) {
|
|
42
|
+
client := &videoSubmitRecordingClient{}
|
|
43
|
+
runner := &common.Runner{
|
|
44
|
+
Client: client,
|
|
45
|
+
Config: &config.Config{Paths: &config.Paths{SubmitRun: "/custom/submit_run?source=a%2Bb&item=1&item=2"}},
|
|
46
|
+
}
|
|
47
|
+
if _, err := Run(context.Background(), &Options{Prompt: "cat walking", Model: model}, runner); err != nil {
|
|
48
|
+
t.Fatal(err)
|
|
49
|
+
}
|
|
50
|
+
path, err := url.Parse(client.path)
|
|
51
|
+
if err != nil {
|
|
52
|
+
t.Fatal(err)
|
|
53
|
+
}
|
|
54
|
+
query := path.Query()
|
|
55
|
+
if path.Path != "/custom/submit_run" || query.Get("source") != "a+b" || len(query["item"]) != 2 {
|
|
56
|
+
t.Fatalf("configured path/query lost: %s", client.path)
|
|
57
|
+
}
|
|
58
|
+
var babi map[string]string
|
|
59
|
+
if err := json.Unmarshal([]byte(query.Get("babi_param")), &babi); err != nil {
|
|
60
|
+
t.Fatal(err)
|
|
61
|
+
}
|
|
62
|
+
for key, want := range map[string]string{
|
|
63
|
+
"scene_lv1": "ai_agent", "scene_lv2": "front_tool", "tool_id": "instant_video",
|
|
64
|
+
"tab_name": "other", "edit_type": "instant_video", "enter_from": "skill",
|
|
65
|
+
} {
|
|
66
|
+
if babi[key] != want {
|
|
67
|
+
t.Fatalf("babi_param[%s] = %q, want %q", key, babi[key], want)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
body := client.body.(map[string]any)
|
|
71
|
+
if body["agent_name"] != common.AgentNameVideoPart || body["message"] != "cat walking" || len(body) != 3 {
|
|
72
|
+
t.Fatalf("ROI must not change the request body contract: %#v", body)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
func TestRunPreservesExplicitROIQuery(t *testing.T) {
|
|
79
|
+
for _, model := range []string{"MiniMax-H3", "Seedance_2.0_mini"} {
|
|
80
|
+
t.Run(model, func(t *testing.T) {
|
|
81
|
+
client := &videoSubmitRecordingClient{}
|
|
82
|
+
raw := `{"scene_lv1":"ai_agent","scene_lv2":"front_tool","tool_id":"custom_video"}`
|
|
83
|
+
runner := &common.Runner{Client: client, Config: &config.Config{Paths: &config.Paths{
|
|
84
|
+
SubmitRun: "/custom/submit_run?babi_param=" + url.QueryEscape(raw),
|
|
85
|
+
}}}
|
|
86
|
+
if _, err := Run(context.Background(), &Options{Prompt: "cat walking", Model: model}, runner); err != nil {
|
|
87
|
+
t.Fatal(err)
|
|
88
|
+
}
|
|
89
|
+
path, err := url.Parse(client.path)
|
|
90
|
+
if err != nil || path.Query().Get("babi_param") != raw {
|
|
91
|
+
t.Fatalf("explicit attribution overwritten: %s, %v", client.path, err)
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func TestRunRejectsMalformedQueryBeforeSubmit(t *testing.T) {
|
|
98
|
+
client := &videoSubmitRecordingClient{}
|
|
99
|
+
runner := &common.Runner{Client: client, Config: &config.Config{Paths: &config.Paths{
|
|
100
|
+
SubmitRun: "/custom/submit_run?source=%invalid",
|
|
101
|
+
}}}
|
|
102
|
+
if _, err := Run(context.Background(), &Options{Prompt: "cat walking", Model: "MiniMax-H3"}, runner); err == nil {
|
|
103
|
+
t.Fatal("expected malformed URL query error")
|
|
104
|
+
}
|
|
105
|
+
if client.path != "" {
|
|
106
|
+
t.Fatal("malformed URL must not submit a request")
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func TestConfiguredVideoModelParametersPassThrough(t *testing.T) {
|
|
111
|
+
for _, model := range []string{"MiniMax-H3", "MiniMax-H3-Max", "wan3.0", "happyhorse-1.1"} {
|
|
112
|
+
t.Run(model, func(t *testing.T) {
|
|
113
|
+
opts := &Options{Prompt: "cat walking", Model: model, Ratio: "16:9"}
|
|
114
|
+
if err := ValidateOptions(opts); err != nil {
|
|
115
|
+
t.Fatal(err)
|
|
116
|
+
}
|
|
117
|
+
data, err := json.Marshal(buildSubmitRunBody(opts, nil, nil, nil))
|
|
118
|
+
if err != nil {
|
|
119
|
+
t.Fatal(err)
|
|
120
|
+
}
|
|
121
|
+
var body struct {
|
|
122
|
+
AgentName string `json:"agent_name"`
|
|
123
|
+
Param map[string]any `json:"video_part_tool_param"`
|
|
124
|
+
}
|
|
125
|
+
if err := json.Unmarshal(data, &body); err != nil {
|
|
126
|
+
t.Fatal(err)
|
|
127
|
+
}
|
|
128
|
+
if body.AgentName != "pippit_video_part_agent" || body.Param["model"] != model || body.Param["ratio"] != "16:9" {
|
|
129
|
+
t.Fatalf("unexpected body: %s", data)
|
|
130
|
+
}
|
|
131
|
+
for _, field := range []string{"duration_sec", "resolution", "generate_type"} {
|
|
132
|
+
if _, exists := body.Param[field]; exists {
|
|
133
|
+
t.Fatalf("CLI must leave %s defaults to the API: %s", field, data)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// API owns the Max 21:9 rejection; the CLI must not add an enum gate.
|
|
137
|
+
opts.Ratio = "21:9"
|
|
138
|
+
if err := ValidateOptions(opts); err != nil {
|
|
139
|
+
t.Fatal(err)
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
package models
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"slices"
|
|
7
|
+
"strconv"
|
|
8
|
+
"strings"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// Mirrors capcut_business_common.Ratio and the Skill API's skillVideoRatio.
|
|
12
|
+
// Custom (1) has no corresponding generate-video --ratio value.
|
|
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
|
+
}
|
|
17
|
+
|
|
18
|
+
type dimensionConfig struct {
|
|
19
|
+
Key string `json:"key"`
|
|
20
|
+
DefaultValue *string `json:"default_value"`
|
|
21
|
+
OptionList []struct {
|
|
22
|
+
Value string `json:"value"`
|
|
23
|
+
Disabled bool `json:"disabled"`
|
|
24
|
+
} `json:"option_list"`
|
|
25
|
+
RangeConfig *struct {
|
|
26
|
+
Min *int64 `json:"min_value"`
|
|
27
|
+
Max *int64 `json:"max_value"`
|
|
28
|
+
Step int64 `json:"step"`
|
|
29
|
+
} `json:"range_config"`
|
|
30
|
+
ActiveWhenAny json.RawMessage `json:"active_when_any"`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Normalize only fields with an explicit CLI contract. RawMessage preserves
|
|
34
|
+
// unknown fields and large integers; the cached catalog is never mutated.
|
|
35
|
+
func describeModel(raw json.RawMessage) (json.RawMessage, error) {
|
|
36
|
+
var fields map[string]json.RawMessage
|
|
37
|
+
var source struct {
|
|
38
|
+
Key string `json:"key"`
|
|
39
|
+
Ratios []int64 `json:"supported_ratio_list"`
|
|
40
|
+
Default *int64 `json:"default_ratio"`
|
|
41
|
+
Parameter struct {
|
|
42
|
+
Dimensions []*dimensionConfig `json:"dimensions"`
|
|
43
|
+
} `json:"parameter_config"`
|
|
44
|
+
Creation *struct {
|
|
45
|
+
Modes []json.RawMessage `json:"modes"`
|
|
46
|
+
} `json:"creation_mode_config"`
|
|
47
|
+
}
|
|
48
|
+
if err := json.Unmarshal(raw, &fields); err != nil {
|
|
49
|
+
return nil, err
|
|
50
|
+
}
|
|
51
|
+
if err := json.Unmarshal(raw, &source); err != nil {
|
|
52
|
+
return nil, fmt.Errorf("模型参数配置无法解析,请 --refresh 重试或升级 CLI: %w", err)
|
|
53
|
+
}
|
|
54
|
+
out := make(map[string]any, len(fields))
|
|
55
|
+
for key, value := range fields {
|
|
56
|
+
out[key] = value
|
|
57
|
+
}
|
|
58
|
+
delete(out, "config_key")
|
|
59
|
+
warnings := []string{}
|
|
60
|
+
warn := func(message string) { warnings = append(warnings, message) }
|
|
61
|
+
ratios := make([]string, 0, len(source.Ratios))
|
|
62
|
+
for _, value := range source.Ratios {
|
|
63
|
+
// Unknown enums and Custom cannot be submitted as CLI ratio strings.
|
|
64
|
+
if value := ratioValues[value]; value != "" && !slices.Contains(ratios, value) {
|
|
65
|
+
ratios = append(ratios, value)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
ratio := map[string]any{"options": ratios}
|
|
69
|
+
if source.Default != nil {
|
|
70
|
+
if value := ratioValues[*source.Default]; value != "" {
|
|
71
|
+
if slices.Contains(ratios, value) {
|
|
72
|
+
ratio["default"] = value
|
|
73
|
+
} else {
|
|
74
|
+
warn("默认比例不在可用比例中,未输出默认值;请 --refresh 重试")
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if _, exists := fields["supported_ratio_list"]; exists || source.Default != nil {
|
|
79
|
+
out["ratio"] = ratio
|
|
80
|
+
}
|
|
81
|
+
delete(out, "supported_ratio_list")
|
|
82
|
+
delete(out, "default_ratio")
|
|
83
|
+
|
|
84
|
+
seen := make(map[string]bool)
|
|
85
|
+
for _, dimension := range source.Parameter.Dimensions {
|
|
86
|
+
if dimension == nil || (dimension.Key != "resolution" && dimension.Key != "duration") {
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
if seen[dimension.Key] {
|
|
90
|
+
return nil, fmt.Errorf("模型配置包含重复 %s 维度,请 --refresh 重试", dimension.Key)
|
|
91
|
+
}
|
|
92
|
+
seen[dimension.Key] = true
|
|
93
|
+
out[dimension.Key] = describeDimension(dimension, warn)
|
|
94
|
+
}
|
|
95
|
+
// Keep unrelated parameter dimensions and combination constraints as supplied.
|
|
96
|
+
if parameterRaw, ok := fields["parameter_config"]; ok && string(parameterRaw) != "null" {
|
|
97
|
+
var parameter map[string]json.RawMessage
|
|
98
|
+
if err := json.Unmarshal(parameterRaw, ¶meter); err != nil {
|
|
99
|
+
return nil, err
|
|
100
|
+
}
|
|
101
|
+
var dimensions []json.RawMessage
|
|
102
|
+
if err := json.Unmarshal(parameter["dimensions"], &dimensions); len(parameter["dimensions"]) != 0 && err != nil {
|
|
103
|
+
return nil, err
|
|
104
|
+
}
|
|
105
|
+
remaining := []json.RawMessage{}
|
|
106
|
+
for _, dimension := range dimensions {
|
|
107
|
+
var key struct {
|
|
108
|
+
Key string `json:"key"`
|
|
109
|
+
}
|
|
110
|
+
if err := json.Unmarshal(dimension, &key); err != nil {
|
|
111
|
+
return nil, err
|
|
112
|
+
}
|
|
113
|
+
if key.Key != "resolution" && key.Key != "duration" {
|
|
114
|
+
remaining = append(remaining, dimension)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
delete(parameter, "dimensions")
|
|
118
|
+
if len(remaining) > 0 {
|
|
119
|
+
parameter["dimensions"], _ = json.Marshal(remaining)
|
|
120
|
+
}
|
|
121
|
+
delete(out, "parameter_config")
|
|
122
|
+
if len(parameter) > 0 {
|
|
123
|
+
out["parameter_config"] = parameter
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if seen["duration"] {
|
|
127
|
+
delete(out, "supported_duration_list")
|
|
128
|
+
delete(out, "default_duration_value")
|
|
129
|
+
} else if _, exists := fields["supported_duration_list"]; exists {
|
|
130
|
+
warn("服务端未提供 duration 参数维度;supported_duration_list 为旧时长枚举,不能直接当作 --duration 秒数")
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
limits := map[string]json.RawMessage{}
|
|
134
|
+
for from, to := range map[string]string{
|
|
135
|
+
"total_limit": "total_limit", "image_total_limit": "image_total_limit",
|
|
136
|
+
"video_total_limit": "video_total_limit", "audio_total_limit": "audio_total_limit",
|
|
137
|
+
"max_image_size": "max_image_size_bytes", "min_video_duration": "min_video_duration_ms",
|
|
138
|
+
"max_video_duration": "max_video_duration_ms", "max_total_video_duration": "max_total_video_duration_ms",
|
|
139
|
+
} {
|
|
140
|
+
if value, exists := fields[from]; exists {
|
|
141
|
+
limits[to] = value
|
|
142
|
+
delete(out, from)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if len(limits) > 0 {
|
|
146
|
+
out["material_limits"] = limits
|
|
147
|
+
}
|
|
148
|
+
if source.Creation != nil {
|
|
149
|
+
modes := make([]map[string]any, 0, len(source.Creation.Modes))
|
|
150
|
+
for _, rawMode := range source.Creation.Modes {
|
|
151
|
+
if string(rawMode) == "null" {
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
var modeFields map[string]json.RawMessage
|
|
155
|
+
var mode struct {
|
|
156
|
+
Key string `json:"key"`
|
|
157
|
+
RatioPolicy struct {
|
|
158
|
+
ValueMode string `json:"value_mode"`
|
|
159
|
+
} `json:"ratio_policy"`
|
|
160
|
+
}
|
|
161
|
+
if err := json.Unmarshal(rawMode, &modeFields); err != nil {
|
|
162
|
+
return nil, err
|
|
163
|
+
}
|
|
164
|
+
if err := json.Unmarshal(rawMode, &mode); err != nil {
|
|
165
|
+
return nil, err
|
|
166
|
+
}
|
|
167
|
+
entry := make(map[string]any, len(modeFields)+2)
|
|
168
|
+
for key, value := range modeFields {
|
|
169
|
+
entry[key] = value
|
|
170
|
+
}
|
|
171
|
+
switch mode.Key {
|
|
172
|
+
case "text_to_video", "reference_generation":
|
|
173
|
+
entry["generate_type"] = 0
|
|
174
|
+
case "first_last_frame":
|
|
175
|
+
entry["generate_type"] = 1
|
|
176
|
+
}
|
|
177
|
+
switch mode.RatioPolicy.ValueMode {
|
|
178
|
+
case "smart":
|
|
179
|
+
entry["ratio"] = map[string]any{"options": []string{"adaptive"}, "default": "adaptive"}
|
|
180
|
+
case "", "inherit":
|
|
181
|
+
entry["ratio"] = ratio
|
|
182
|
+
if mode.Key == "text_to_video" && (source.Key == "MiniMax-H3" || source.Key == "MiniMax-H3-Max") {
|
|
183
|
+
fixed := make([]string, 0, len(ratios))
|
|
184
|
+
for _, value := range ratios {
|
|
185
|
+
if value != "adaptive" {
|
|
186
|
+
fixed = append(fixed, value)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
modeRatio := map[string]any{"options": fixed}
|
|
190
|
+
if value, ok := ratio["default"].(string); ok && value != "adaptive" {
|
|
191
|
+
modeRatio["default"] = value
|
|
192
|
+
}
|
|
193
|
+
entry["ratio"] = modeRatio
|
|
194
|
+
}
|
|
195
|
+
default:
|
|
196
|
+
warn("创作模式 " + mode.Key + " 的比例策略无法识别,请 --refresh 重试或升级 CLI")
|
|
197
|
+
}
|
|
198
|
+
modes = append(modes, entry)
|
|
199
|
+
}
|
|
200
|
+
// Retain schema/default-mode metadata, but expose interpreted modes separately.
|
|
201
|
+
var creation map[string]json.RawMessage
|
|
202
|
+
if err := json.Unmarshal(fields["creation_mode_config"], &creation); err != nil {
|
|
203
|
+
return nil, err
|
|
204
|
+
}
|
|
205
|
+
delete(creation, "modes")
|
|
206
|
+
delete(out, "creation_mode_config")
|
|
207
|
+
if len(creation) > 0 {
|
|
208
|
+
out["creation_mode_config"] = creation
|
|
209
|
+
}
|
|
210
|
+
out["creation_modes"] = modes
|
|
211
|
+
}
|
|
212
|
+
if (source.Key == "MiniMax-H3" || source.Key == "MiniMax-H3-Max") && slices.Contains(ratios, "adaptive") {
|
|
213
|
+
out["notes"] = []string{"MiniMax 使用 adaptive 需要参考图片或视频;纯文生视频请选择固定比例。创作模式的比例策略优先于模型级选项。"}
|
|
214
|
+
}
|
|
215
|
+
if len(warnings) > 0 {
|
|
216
|
+
out["warnings"] = warnings
|
|
217
|
+
}
|
|
218
|
+
return json.Marshal(out)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
func describeDimension(d *dimensionConfig, warn func(string)) map[string]any {
|
|
222
|
+
out := map[string]any{}
|
|
223
|
+
if len(d.ActiveWhenAny) > 0 {
|
|
224
|
+
out["active_when_any"] = d.ActiveWhenAny
|
|
225
|
+
}
|
|
226
|
+
convert := func(value string) (any, bool) {
|
|
227
|
+
value = strings.TrimSpace(value)
|
|
228
|
+
if d.Key == "duration" {
|
|
229
|
+
number, err := strconv.ParseInt(value, 10, 32)
|
|
230
|
+
return number, err == nil && number > 0
|
|
231
|
+
}
|
|
232
|
+
return strings.ToLower(value), value != ""
|
|
233
|
+
}
|
|
234
|
+
if d.Key == "duration" {
|
|
235
|
+
out["unit"] = "seconds"
|
|
236
|
+
}
|
|
237
|
+
validDefault := func(any) bool { return false }
|
|
238
|
+
if bounds := d.RangeConfig; bounds != nil {
|
|
239
|
+
if d.Key != "duration" || len(d.OptionList) != 0 || bounds.Min == nil || bounds.Max == nil || *bounds.Min <= 0 || *bounds.Max < *bounds.Min {
|
|
240
|
+
warn(d.Key + " 范围配置不合法,请 --refresh 重试")
|
|
241
|
+
return out
|
|
242
|
+
}
|
|
243
|
+
step := bounds.Step
|
|
244
|
+
if step < 1 {
|
|
245
|
+
step = 1
|
|
246
|
+
}
|
|
247
|
+
out["min"], out["max"], out["step"] = *bounds.Min, *bounds.Max, step
|
|
248
|
+
validDefault = func(value any) bool {
|
|
249
|
+
n := value.(int64)
|
|
250
|
+
return n >= *bounds.Min && n <= *bounds.Max && (n-*bounds.Min)%step == 0
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
options := []any{}
|
|
254
|
+
for _, option := range d.OptionList {
|
|
255
|
+
if option.Disabled {
|
|
256
|
+
continue
|
|
257
|
+
}
|
|
258
|
+
value, ok := convert(option.Value)
|
|
259
|
+
if !ok {
|
|
260
|
+
warn(d.Key + " 包含无法转换的选项,已排除;请 --refresh 重试")
|
|
261
|
+
continue
|
|
262
|
+
}
|
|
263
|
+
if !slices.Contains(options, value) {
|
|
264
|
+
options = append(options, value)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
out["options"] = options
|
|
268
|
+
if len(options) == 0 {
|
|
269
|
+
warn(d.Key + " 没有可用选项,请 --refresh 重试")
|
|
270
|
+
}
|
|
271
|
+
validDefault = func(value any) bool { return slices.Contains(options, value) }
|
|
272
|
+
}
|
|
273
|
+
if d.DefaultValue != nil {
|
|
274
|
+
if value, ok := convert(*d.DefaultValue); ok && validDefault(value) {
|
|
275
|
+
out["default"] = value
|
|
276
|
+
} else {
|
|
277
|
+
warn(d.Key + " 默认值不在可用范围内,未输出默认值;请 --refresh 重试")
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return out
|
|
281
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
package models
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"reflect"
|
|
7
|
+
"testing"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func description(t *testing.T, raw string) map[string]json.RawMessage {
|
|
11
|
+
t.Helper()
|
|
12
|
+
out, err := describeModel(json.RawMessage(raw))
|
|
13
|
+
if err != nil {
|
|
14
|
+
t.Fatal(err)
|
|
15
|
+
}
|
|
16
|
+
var fields map[string]json.RawMessage
|
|
17
|
+
if err := json.Unmarshal(out, &fields); err != nil {
|
|
18
|
+
t.Fatal(err)
|
|
19
|
+
}
|
|
20
|
+
return fields
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func TestDescriptionCLIParametersAndRawPreservation(t *testing.T) {
|
|
24
|
+
raw := `{"key":"MiniMax-H3","supported_ratio_list":[0,2,13,3,4,5,6],"default_ratio":3,"config_key":"internal",
|
|
25
|
+
"future_field":9007199254740993,"audio_total_limit":0,"max_image_size":31457280,"min_video_duration":2000,
|
|
26
|
+
"supported_duration_list":[{"value":999}],"default_duration_value":999,
|
|
27
|
+
"parameter_config":{"dimensions":[
|
|
28
|
+
{"key":"duration","default_value":"10","range_config":{"min_value":4,"max_value":15,"step":1}},
|
|
29
|
+
{"key":"resolution","default_value":" 768P ","option_list":[{"value":"768p"},{"value":"2k"},{"value":"4k","disabled":true}]},
|
|
30
|
+
{"key":"seed","default_value":"random"}],"need_available_combinations":true}}
|
|
31
|
+
`
|
|
32
|
+
out := description(t, raw)
|
|
33
|
+
for _, key := range []string{"config_key", "supported_ratio_list", "default_ratio", "supported_duration_list", "default_duration_value", "audio_total_limit", "max_image_size"} {
|
|
34
|
+
if _, ok := out[key]; ok {
|
|
35
|
+
t.Fatalf("unconverted field %s", key)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
var ratio struct {
|
|
39
|
+
Options []string
|
|
40
|
+
Default string
|
|
41
|
+
}
|
|
42
|
+
if err := json.Unmarshal(out["ratio"], &ratio); err != nil {
|
|
43
|
+
t.Fatal(err)
|
|
44
|
+
}
|
|
45
|
+
if !reflect.DeepEqual(ratio.Options, []string{"adaptive", "16:9", "21:9", "9:16", "4:3", "3:4", "1:1"}) || ratio.Default != "9:16" {
|
|
46
|
+
t.Fatalf("ratio=%+v", ratio)
|
|
47
|
+
}
|
|
48
|
+
var resolution struct {
|
|
49
|
+
Options []string
|
|
50
|
+
Default string
|
|
51
|
+
}
|
|
52
|
+
if err := json.Unmarshal(out["resolution"], &resolution); err != nil {
|
|
53
|
+
t.Fatal(err)
|
|
54
|
+
}
|
|
55
|
+
if !reflect.DeepEqual(resolution.Options, []string{"768p", "2k"}) || resolution.Default != "768p" {
|
|
56
|
+
t.Fatalf("resolution=%+v", resolution)
|
|
57
|
+
}
|
|
58
|
+
var duration struct {
|
|
59
|
+
Min, Max, Step, Default int
|
|
60
|
+
Unit string
|
|
61
|
+
}
|
|
62
|
+
if err := json.Unmarshal(out["duration"], &duration); err != nil {
|
|
63
|
+
t.Fatal(err)
|
|
64
|
+
}
|
|
65
|
+
if duration.Min != 4 || duration.Max != 15 || duration.Step != 1 || duration.Default != 10 || duration.Unit != "seconds" {
|
|
66
|
+
t.Fatalf("duration=%+v", duration)
|
|
67
|
+
}
|
|
68
|
+
if string(out["future_field"]) != "9007199254740993" {
|
|
69
|
+
t.Fatal("integer precision lost")
|
|
70
|
+
}
|
|
71
|
+
var limits map[string]json.RawMessage
|
|
72
|
+
if err := json.Unmarshal(out["material_limits"], &limits); err != nil {
|
|
73
|
+
t.Fatal(err)
|
|
74
|
+
}
|
|
75
|
+
if string(limits["audio_total_limit"]) != "0" || string(limits["max_image_size_bytes"]) != "31457280" || string(limits["min_video_duration_ms"]) != "2000" {
|
|
76
|
+
t.Fatalf("limits=%s", out["material_limits"])
|
|
77
|
+
}
|
|
78
|
+
if _, ok := limits["video_total_limit"]; ok {
|
|
79
|
+
t.Fatal("absent limit must stay absent")
|
|
80
|
+
}
|
|
81
|
+
if !bytes.Contains(out["parameter_config"], []byte("seed")) || !bytes.Contains(out["parameter_config"], []byte("need_available_combinations")) {
|
|
82
|
+
t.Fatal("unrelated configuration lost")
|
|
83
|
+
}
|
|
84
|
+
if _, ok := out["warnings"]; ok {
|
|
85
|
+
t.Fatalf("valid dimensions must override legacy durations: %s", out["warnings"])
|
|
86
|
+
}
|
|
87
|
+
if !bytes.Contains(out["notes"], []byte("纯文生视频")) {
|
|
88
|
+
t.Fatal("missing MiniMax adaptive restriction")
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func TestDescriptionUnknownRatioAndDefault(t *testing.T) {
|
|
93
|
+
for _, value := range []int{1, 999} {
|
|
94
|
+
raw, _ := json.Marshal(map[string]any{"supported_ratio_list": []int{value, 2, 2}, "default_ratio": value})
|
|
95
|
+
out := description(t, string(raw))
|
|
96
|
+
var ratio struct {
|
|
97
|
+
Options []string
|
|
98
|
+
Default *string
|
|
99
|
+
}
|
|
100
|
+
if err := json.Unmarshal(out["ratio"], &ratio); err != nil {
|
|
101
|
+
t.Fatal(err)
|
|
102
|
+
}
|
|
103
|
+
if !reflect.DeepEqual(ratio.Options, []string{"16:9"}) || ratio.Default != nil {
|
|
104
|
+
t.Fatalf("invented usable value: %+v", ratio)
|
|
105
|
+
}
|
|
106
|
+
if _, exists := out["warnings"]; exists {
|
|
107
|
+
t.Fatal("unknown and custom enums should be silently skipped")
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
out := description(t, `{"supported_ratio_list":[0,7,8,9,10,11,12],"default_ratio":0}`)
|
|
111
|
+
var ratio struct {
|
|
112
|
+
Options []string
|
|
113
|
+
Default string
|
|
114
|
+
}
|
|
115
|
+
if err := json.Unmarshal(out["ratio"], &ratio); err != nil {
|
|
116
|
+
t.Fatal(err)
|
|
117
|
+
}
|
|
118
|
+
if ratio.Default != "adaptive" || !reflect.DeepEqual(ratio.Options, []string{"adaptive", "2:1", "2.35:1", "1.85:1", "1.125:2.436", "3:2", "2:3"}) {
|
|
119
|
+
t.Fatalf("ratio=%+v", ratio)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func TestDescriptionDimensionFailuresAndOptions(t *testing.T) {
|
|
124
|
+
for _, dimension := range []string{
|
|
125
|
+
`{"key":"resolution","default_value":"2k","option_list":[{"value":"768p"},{"value":"2k","disabled":true}]}`,
|
|
126
|
+
`{"key":"resolution","default_value":"","option_list":[]}`,
|
|
127
|
+
`{"key":"duration","default_value":"7","range_config":{"min_value":4,"max_value":10,"step":2}}`,
|
|
128
|
+
`{"key":"duration","range_config":{"min_value":10,"max_value":4}}`,
|
|
129
|
+
`{"key":"duration","option_list":[{"value":"auto"}]}`,
|
|
130
|
+
} {
|
|
131
|
+
out := description(t, `{"parameter_config":{"dimensions":[`+dimension+`]}}`)
|
|
132
|
+
if _, ok := out["warnings"]; !ok {
|
|
133
|
+
t.Fatalf("missing warning: %s", dimension)
|
|
134
|
+
}
|
|
135
|
+
for _, key := range []string{"resolution", "duration"} {
|
|
136
|
+
var value map[string]json.RawMessage
|
|
137
|
+
if len(out[key]) == 0 {
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
if err := json.Unmarshal(out[key], &value); err != nil {
|
|
141
|
+
t.Fatal(err)
|
|
142
|
+
}
|
|
143
|
+
if _, ok := value["default"]; ok {
|
|
144
|
+
t.Fatalf("invalid default exposed: %s", out[key])
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
out := description(t, `{"parameter_config":{"dimensions":[{"key":"duration","default_value":"10","option_list":[{"value":"5"},{"value":"10"},{"value":"15","disabled":true}]}]}}`)
|
|
149
|
+
var duration struct {
|
|
150
|
+
Options []int
|
|
151
|
+
Default int
|
|
152
|
+
Min *int
|
|
153
|
+
}
|
|
154
|
+
if err := json.Unmarshal(out["duration"], &duration); err != nil {
|
|
155
|
+
t.Fatal(err)
|
|
156
|
+
}
|
|
157
|
+
if !reflect.DeepEqual(duration.Options, []int{5, 10}) || duration.Default != 10 || duration.Min != nil {
|
|
158
|
+
t.Fatalf("duration=%+v", duration)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
func TestDescriptionCreationModesAndCacheNotMutated(t *testing.T) {
|
|
163
|
+
raw := json.RawMessage(`{"key":"MiniMax-H3-Max","kind":"video","supported_ratio_list":[0,2],"default_ratio":2,"creation_mode_config":{"schema_version":1,"default_mode":"text_to_video","modes":[{"key":"text_to_video","enabled":true,"ratio_policy":{"value_mode":"inherit"}},{"key":"first_last_frame","enabled":true,"ratio_policy":{"value_mode":"smart"},"validation_policy":{"required_video_count":0,"forbidden_input_types":["video","audio"]}}]}}`)
|
|
164
|
+
before := append([]byte(nil), raw...)
|
|
165
|
+
var catalog Catalog
|
|
166
|
+
if err := json.Unmarshal([]byte(`{"config":{"models":[]}}`), &catalog); err != nil {
|
|
167
|
+
t.Fatal(err)
|
|
168
|
+
}
|
|
169
|
+
catalog.Config.Models = append(catalog.Config.Models, raw)
|
|
170
|
+
result, err := catalog.Describe("MiniMax-H3-Max")
|
|
171
|
+
if err != nil {
|
|
172
|
+
t.Fatal(err)
|
|
173
|
+
}
|
|
174
|
+
var out struct {
|
|
175
|
+
Modes []struct {
|
|
176
|
+
GenerateType int `json:"generate_type"`
|
|
177
|
+
Ratio struct {
|
|
178
|
+
Options []string
|
|
179
|
+
Default string
|
|
180
|
+
}
|
|
181
|
+
Validation struct {
|
|
182
|
+
Required *int `json:"required_video_count"`
|
|
183
|
+
Forbidden []string `json:"forbidden_input_types"`
|
|
184
|
+
} `json:"validation_policy"`
|
|
185
|
+
} `json:"creation_modes"`
|
|
186
|
+
}
|
|
187
|
+
if err := json.Unmarshal(result, &out); err != nil {
|
|
188
|
+
t.Fatal(err)
|
|
189
|
+
}
|
|
190
|
+
if len(out.Modes) != 2 || out.Modes[0].Ratio.Default != "16:9" || !reflect.DeepEqual(out.Modes[0].Ratio.Options, []string{"16:9"}) || out.Modes[1].GenerateType != 1 || out.Modes[1].Ratio.Default != "adaptive" || !reflect.DeepEqual(out.Modes[1].Ratio.Options, []string{"adaptive"}) {
|
|
191
|
+
t.Fatalf("modes=%+v", out.Modes)
|
|
192
|
+
}
|
|
193
|
+
if out.Modes[1].Validation.Required == nil || *out.Modes[1].Validation.Required != 0 || !reflect.DeepEqual(out.Modes[1].Validation.Forbidden, []string{"video", "audio"}) {
|
|
194
|
+
t.Fatal("mode requirements lost")
|
|
195
|
+
}
|
|
196
|
+
if !bytes.Equal(before, catalog.Config.Models[0]) {
|
|
197
|
+
t.Fatal("description mutated cached raw config")
|
|
198
|
+
}
|
|
199
|
+
}
|